diff --git a/.air.toml b/.air.toml
index d13f8c4f99..de97bd8b29 100644
--- a/.air.toml
+++ b/.air.toml
@@ -8,6 +8,15 @@ delay = 1000
 include_ext = ["go", "tmpl"]
 include_file = ["main.go"]
 include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
-exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata", "models/fixtures", "models/migrations/fixtures", "modules/migration/file_format_testdata", "modules/avatar/identicon/testdata"]
+exclude_dir = [
+  "models/fixtures",
+  "models/migrations/fixtures",
+  "modules/avatar/identicon/testdata",
+  "modules/avatar/testdata",
+  "modules/git/tests",
+  "modules/migration/file_format_testdata",
+  "routers/private/tests",
+  "services/gitdiff/testdata",
+]
 exclude_regex = ["_test.go$", "_gen.go$"]
 stop_on_error = true
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 9e290fb6a5..d391cf78cf 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,14 +1,16 @@
 {
   "name": "Gitea DevContainer",
-  "image": "mcr.microsoft.com/devcontainers/go:1.21-bullseye",
+  "image": "mcr.microsoft.com/devcontainers/go:1.22-bullseye",
   "features": {
     // installs nodejs into container
     "ghcr.io/devcontainers/features/node:1": {
-      "version":"20"
+      "version": "20"
     },
     "ghcr.io/devcontainers/features/git-lfs:1.1.0": {},
     "ghcr.io/devcontainers-contrib/features/poetry:2": {},
-    "ghcr.io/devcontainers/features/python:1": {}
+    "ghcr.io/devcontainers/features/python:1": {
+      "version": "3.12"
+    }
   },
   "customizations": {
     "vscode": {
@@ -22,7 +24,7 @@
         "DavidAnson.vscode-markdownlint",
         "Vue.volar",
         "ms-azuretools.vscode-docker",
-        "zixuanchen.vitest-explorer",
+        "vitest.explorer",
         "qwtel.sqlite-viewer",
         "GitHub.vscode-pull-request-github"
       ]
diff --git a/.dockerignore b/.dockerignore
index 80cbeb040c..b696e1603c 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -14,7 +14,7 @@ _test
 
 # MS VSCode
 .vscode
-__debug_bin
+__debug_bin*
 
 # Architecture specific extensions/prefixes
 *.[568vq]
@@ -62,7 +62,6 @@ cpu.out
 /data
 /indexers
 /log
-/public/img/avatar
 /tests/integration/gitea-integration-*
 /tests/integration/indexers-*
 /tests/e2e/gitea-e2e-*
@@ -78,7 +77,7 @@ cpu.out
 /public/assets/js
 /public/assets/css
 /public/assets/fonts
-/public/assets/img/webpack
+/public/assets/img/avatar
 /vendor
 /web_src/fomantic/node_modules
 /web_src/fomantic/build/*
@@ -96,6 +95,9 @@ cpu.out
 /.air
 /.go-licenses
 
+# Files and folders that were previously generated
+/public/assets/img/webpack
+
 # Snapcraft
 snap/.snapcraft/
 parts/
diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index fc6f38ec53..5fd0a245f2 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -3,6 +3,7 @@ reportUnusedDisableDirectives: true
 
 ignorePatterns:
   - /web_src/js/vendor
+  - /web_src/fomantic
 
 parserOptions:
   sourceType: module
@@ -12,6 +13,7 @@ plugins:
   - "@eslint-community/eslint-plugin-eslint-comments"
   - "@stylistic/eslint-plugin-js"
   - eslint-plugin-array-func
+  - eslint-plugin-github
   - eslint-plugin-i
   - eslint-plugin-jquery
   - eslint-plugin-no-jquery
@@ -41,10 +43,6 @@ overrides:
       worker: true
     rules:
       no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top]
-  - files: ["build/generate-images.js"]
-    rules:
-      i/no-unresolved: [0]
-      i/no-extraneous-dependencies: [0]
   - files: ["*.config.*"]
     rules:
       i/no-unused-modules: [0]
@@ -122,7 +120,7 @@ rules:
   "@stylistic/js/arrow-spacing": [2, {before: true, after: true}]
   "@stylistic/js/block-spacing": [0]
   "@stylistic/js/brace-style": [2, 1tbs, {allowSingleLine: true}]
-  "@stylistic/js/comma-dangle": [2, only-multiline]
+  "@stylistic/js/comma-dangle": [2, always-multiline]
   "@stylistic/js/comma-spacing": [2, {before: false, after: true}]
   "@stylistic/js/comma-style": [2, last]
   "@stylistic/js/computed-property-spacing": [2, never]
@@ -170,7 +168,7 @@ rules:
   "@stylistic/js/semi-spacing": [2, {before: false, after: true}]
   "@stylistic/js/semi-style": [2, last]
   "@stylistic/js/space-before-blocks": [2, always]
-  "@stylistic/js/space-before-function-paren": [0]
+  "@stylistic/js/space-before-function-paren": [2, {anonymous: ignore, named: never, asyncArrow: always}]
   "@stylistic/js/space-in-parens": [2, never]
   "@stylistic/js/space-infix-ops": [2]
   "@stylistic/js/space-unary-ops": [2]
@@ -209,6 +207,29 @@ rules:
   func-names: [0]
   func-style: [0]
   getter-return: [2]
+  github/a11y-aria-label-is-well-formatted: [0]
+  github/a11y-no-title-attribute: [0]
+  github/a11y-no-visually-hidden-interactive-element: [0]
+  github/a11y-role-supports-aria-props: [0]
+  github/a11y-svg-has-accessible-name: [0]
+  github/array-foreach: [0]
+  github/async-currenttarget: [2]
+  github/async-preventdefault: [2]
+  github/authenticity-token: [0]
+  github/get-attribute: [0]
+  github/js-class-name: [0]
+  github/no-blur: [0]
+  github/no-d-none: [0]
+  github/no-dataset: [2]
+  github/no-dynamic-script-tag: [2]
+  github/no-implicit-buggy-globals: [2]
+  github/no-inner-html: [0]
+  github/no-innerText: [2]
+  github/no-then: [2]
+  github/no-useless-passive: [2]
+  github/prefer-observers: [2]
+  github/require-passive-events: [2]
+  github/unescaped-html-literal: [0]
   grouped-accessor-pairs: [2]
   guard-for-in: [0]
   id-blacklist: [0]
@@ -259,20 +280,20 @@ rules:
   i/unambiguous: [0]
   init-declarations: [0]
   jquery/no-ajax-events: [2]
-  jquery/no-ajax: [0]
+  jquery/no-ajax: [2]
   jquery/no-animate: [2]
-  jquery/no-attr: [0]
+  jquery/no-attr: [2]
   jquery/no-bind: [2]
   jquery/no-class: [0]
   jquery/no-clone: [2]
   jquery/no-closest: [0]
-  jquery/no-css: [0]
+  jquery/no-css: [2]
   jquery/no-data: [0]
   jquery/no-deferred: [2]
   jquery/no-delegate: [2]
   jquery/no-each: [0]
   jquery/no-extend: [2]
-  jquery/no-fade: [0]
+  jquery/no-fade: [2]
   jquery/no-filter: [0]
   jquery/no-find: [0]
   jquery/no-global-eval: [2]
@@ -283,15 +304,15 @@ rules:
   jquery/no-in-array: [2]
   jquery/no-is-array: [2]
   jquery/no-is-function: [2]
-  jquery/no-is: [0]
+  jquery/no-is: [2]
   jquery/no-load: [2]
-  jquery/no-map: [0]
+  jquery/no-map: [2]
   jquery/no-merge: [2]
   jquery/no-param: [2]
   jquery/no-parent: [0]
   jquery/no-parents: [0]
   jquery/no-parse-html: [2]
-  jquery/no-prop: [0]
+  jquery/no-prop: [2]
   jquery/no-proxy: [2]
   jquery/no-ready: [2]
   jquery/no-serialize: [2]
@@ -372,12 +393,12 @@ rules:
   no-irregular-whitespace: [2]
   no-iterator: [2]
   no-jquery/no-ajax-events: [2]
-  no-jquery/no-ajax: [0]
+  no-jquery/no-ajax: [2]
   no-jquery/no-and-self: [2]
   no-jquery/no-animate-toggle: [2]
   no-jquery/no-animate: [2]
-  no-jquery/no-append-html: [0]
-  no-jquery/no-attr: [0]
+  no-jquery/no-append-html: [2]
+  no-jquery/no-attr: [2]
   no-jquery/no-bind: [2]
   no-jquery/no-box-model: [2]
   no-jquery/no-browser: [2]
@@ -389,7 +410,7 @@ rules:
   no-jquery/no-constructor-attributes: [2]
   no-jquery/no-contains: [2]
   no-jquery/no-context-prop: [2]
-  no-jquery/no-css: [0]
+  no-jquery/no-css: [2]
   no-jquery/no-data: [0]
   no-jquery/no-deferred: [2]
   no-jquery/no-delegate: [2]
@@ -420,14 +441,14 @@ rules:
   no-jquery/no-is-numeric: [2]
   no-jquery/no-is-plain-object: [2]
   no-jquery/no-is-window: [2]
-  no-jquery/no-is: [0]
+  no-jquery/no-is: [2]
   no-jquery/no-jquery-constructor: [0]
   no-jquery/no-live: [2]
   no-jquery/no-load-shorthand: [2]
   no-jquery/no-load: [2]
   no-jquery/no-map-collection: [0]
   no-jquery/no-map-util: [2]
-  no-jquery/no-map: [0]
+  no-jquery/no-map: [2]
   no-jquery/no-merge: [2]
   no-jquery/no-node-name: [2]
   no-jquery/no-noop: [2]
@@ -442,7 +463,7 @@ rules:
   no-jquery/no-parse-html: [2]
   no-jquery/no-parse-json: [2]
   no-jquery/no-parse-xml: [2]
-  no-jquery/no-prop: [0]
+  no-jquery/no-prop: [2]
   no-jquery/no-proxy: [2]
   no-jquery/no-ready-shorthand: [2]
   no-jquery/no-ready: [2]
@@ -463,7 +484,7 @@ rules:
   no-jquery/no-visibility: [2]
   no-jquery/no-when: [2]
   no-jquery/no-wrap: [2]
-  no-jquery/variable-pattern: [0]
+  no-jquery/variable-pattern: [2]
   no-label-var: [2]
   no-labels: [0] # handled by no-restricted-syntax
   no-lone-blocks: [2]
@@ -516,7 +537,7 @@ rules:
   no-underscore-dangle: [0]
   no-unexpected-multiline: [2]
   no-unmodified-loop-condition: [2]
-  no-unneeded-ternary: [0]
+  no-unneeded-ternary: [2]
   no-unreachable-loop: [2]
   no-unreachable: [2]
   no-unsafe-finally: [2]
@@ -558,7 +579,6 @@ rules:
   prefer-rest-params: [2]
   prefer-spread: [2]
   prefer-template: [2]
-  quotes: [2, single, {avoidEscape: true, allowTemplateLiterals: true}]
   radix: [2, as-needed]
   regexp/confusing-quantifier: [2]
   regexp/control-character-escape: [2]
@@ -696,12 +716,14 @@ rules:
   unicorn/import-style: [0]
   unicorn/new-for-builtins: [2]
   unicorn/no-abusive-eslint-disable: [0]
+  unicorn/no-anonymous-default-export: [0]
   unicorn/no-array-callback-reference: [0]
   unicorn/no-array-for-each: [2]
   unicorn/no-array-method-this-argument: [2]
   unicorn/no-array-push-push: [2]
   unicorn/no-array-reduce: [2]
   unicorn/no-await-expression-member: [0]
+  unicorn/no-await-in-promise-methods: [2]
   unicorn/no-console-spaces: [0]
   unicorn/no-document-cookie: [2]
   unicorn/no-empty-file: [2]
@@ -718,6 +740,7 @@ rules:
   unicorn/no-null: [0]
   unicorn/no-object-as-default-parameter: [0]
   unicorn/no-process-exit: [0]
+  unicorn/no-single-promise-in-promise-methods: [2]
   unicorn/no-static-only-class: [2]
   unicorn/no-thenable: [2]
   unicorn/no-this-assignment: [2]
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 624a2d97db..1447a6ea32 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,2 +1 @@
 open_collective: gitea
-custom: https://www.bountysource.com/teams/gitea
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 8a5ab26975..d1b4d00d80 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,36 +1,77 @@
 modifies/docs:
-  - "**/*.md"
-  - "docs/**"
-
-modifies/frontend:
-  - "web_src/**/*"
+  - changed-files:
+      - any-glob-to-any-file:
+          - "**/*.md"
+          - "docs/**"
 
 modifies/templates:
-  - all: ["templates/**", "!templates/swagger/v1_json.tmpl"]
+  - changed-files:
+      - all-globs-to-any-file:
+          - "templates/**"
+          - "!templates/swagger/v1_json.tmpl"
 
 modifies/api:
-  - "routers/api/**"
-  - "templates/swagger/v1_json.tmpl"
+  - changed-files:
+      - any-glob-to-any-file:
+          - "routers/api/**"
+          - "templates/swagger/v1_json.tmpl"
 
 modifies/cli:
-  - "cmd/**"
+  - changed-files:
+      - any-glob-to-any-file:
+          - "cmd/**"
 
 modifies/translation:
-  - "options/locale/*.ini"
+  - changed-files:
+      - any-glob-to-any-file:
+          - "options/locale/*.ini"
 
 modifies/migrations:
-  - "models/migrations/**/*"
+  - changed-files:
+      - any-glob-to-any-file:
+          - "models/migrations/**"
 
 modifies/internal:
-  - "Makefile"
-  - "Dockerfile"
-  - "Dockerfile.rootless"
-  - "docker/**"
-  - "webpack.config.js"
-  - ".eslintrc.yaml"
-  - ".golangci.yml"
-  - ".markdownlint.yaml"
-  - ".spectral.yaml"
-  - ".stylelintrc.yaml"
-  - ".yamllint.yaml"
-  - ".github/**"
+  - changed-files:
+      - any-glob-to-any-file:
+          - ".air.toml"
+          - "Makefile"
+          - "Dockerfile"
+          - "Dockerfile.rootless"
+          - ".dockerignore"
+          - "docker/**"
+          - ".editorconfig"
+          - ".eslintrc.yaml"
+          - ".golangci.yml"
+          - ".gitpod.yml"
+          - ".markdownlint.yaml"
+          - ".spectral.yaml"
+          - "stylelint.config.js"
+          - ".yamllint.yaml"
+          - ".github/**"
+          - ".gitea/"
+          - ".devcontainer/**"
+          - "build.go"
+          - "build/**"
+          - "contrib/**"
+
+modifies/dependencies:
+  - changed-files:
+      - any-glob-to-any-file:
+          - "package.json"
+          - "package-lock.json"
+          - "pyproject.toml"
+          - "poetry.lock"
+          - "go.mod"
+          - "go.sum"
+
+modifies/go:
+  - changed-files:
+      - any-glob-to-any-file:
+          - "**/*.go"
+
+modifies/js:
+  - changed-files:
+      - any-glob-to-any-file:
+          - "**/*.js"
+          - "**/*.vue"
diff --git a/.github/workflows/cron-lock.yml b/.github/workflows/cron-lock.yml
deleted file mode 100644
index 746ec49bc6..0000000000
--- a/.github/workflows/cron-lock.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: cron-lock
-
-on:
-  schedule:
-    - cron: "0 0 * * *" # every day at 00:00 UTC
-  workflow_dispatch:
-
-permissions:
-  issues: write
-  pull-requests: write
-
-concurrency:
-  group: lock
-
-jobs:
-  action:
-    runs-on: ubuntu-latest
-    if: github.repository == 'go-gitea/gitea'
-    steps:
-      - uses: dessant/lock-threads@v5
-        with:
-          issue-inactive-days: 45
diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml
index 390aae7c07..f1b51debf1 100644
--- a/.github/workflows/cron-translations.yml
+++ b/.github/workflows/cron-translations.yml
@@ -11,14 +11,19 @@ jobs:
     if: github.repository == 'go-gitea/gitea'
     steps:
       - uses: actions/checkout@v4
-      - name: download from crowdin
-        uses: docker://jonasfranz/crowdin
+      - uses: crowdin/github-action@v1
+        with:
+          upload_sources: true
+          upload_translations: false
+          download_sources: false
+          download_translations: true
+          push_translations: false
+          push_sources: false
+          create_pull_request: false
+          config: crowdin.yml
         env:
+          CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
           CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }}
-          PLUGIN_DOWNLOAD: true
-          PLUGIN_EXPORT_DIR: options/locale/
-          PLUGIN_IGNORE_BRANCH: true
-          PLUGIN_PROJECT_IDENTIFIER: gitea
       - name: update locales
         run: ./build/update-locales.sh
       - name: push translations to repo
@@ -31,19 +36,3 @@ jobs:
           commit_message: "[skip ci] Updated translations via Crowdin"
           remote: "git@github.com:go-gitea/gitea.git"
           ssh_key: ${{ secrets.DEPLOY_KEY }}
-  crowdin-push:
-    runs-on: ubuntu-latest
-    if: github.repository == 'go-gitea/gitea'
-    steps:
-      - uses: actions/checkout@v4
-      - name: push translations to crowdin
-        uses: docker://jonasfranz/crowdin
-        env:
-          CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }}
-          PLUGIN_UPLOAD: true
-          PLUGIN_EXPORT_DIR: options/locale/
-          PLUGIN_IGNORE_BRANCH: true
-          PLUGIN_PROJECT_IDENTIFIER: gitea
-          PLUGIN_FILES: |
-            locale_en-US.ini: options/locale/locale_en-US.ini
-          PLUGIN_BRANCH: main
diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml
index c909f78597..9a609e0551 100644
--- a/.github/workflows/files-changed.yml
+++ b/.github/workflows/files-changed.yml
@@ -48,6 +48,7 @@ jobs:
               - "Makefile"
               - ".golangci.yml"
               - ".editorconfig"
+              - "options/locale/locale_en-US.ini"
 
             frontend:
               - "**/*.js"
@@ -57,7 +58,7 @@ jobs:
               - "package-lock.json"
               - "Makefile"
               - ".eslintrc.yaml"
-              - ".stylelintrc.yaml"
+              - "stylelint.config.js"
               - ".npmrc"
 
             docs:
@@ -72,6 +73,7 @@ jobs:
               - "Makefile"
 
             templates:
+              - "tools/lint-templates-*.js"
               - "templates/**/*.tmpl"
               - "pyproject.toml"
               - "poetry.lock"
diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml
index 0472d9a9f0..99a69ab174 100644
--- a/.github/workflows/pull-compliance.yml
+++ b/.github/workflows/pull-compliance.yml
@@ -32,11 +32,15 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
-          python-version: "3.11"
+          python-version: "3.12"
+      - uses: actions/setup-node@v4
+        with:
+          node-version: 20
       - run: pip install poetry
       - run: make deps-py
+      - run: make deps-frontend
       - run: make lint-templates
 
   lint-yaml:
@@ -45,9 +49,9 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
-          python-version: "3.11"
+          python-version: "3.12"
       - run: pip install poetry
       - run: make deps-py
       - run: make lint-yaml
@@ -64,6 +68,18 @@ jobs:
       - run: make deps-frontend
       - run: make lint-swagger
 
+  lint-spell:
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true'
+    needs: files-changed
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+          check-latest: true
+      - run: make lint-spell
+
   lint-go-windows:
     if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
     needs: files-changed
diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml
index a3886bf618..61c0391509 100644
--- a/.github/workflows/pull-db-tests.yml
+++ b/.github/workflows/pull-db-tests.yml
@@ -49,7 +49,10 @@ jobs:
       - run: make backend
         env:
           TAGS: bindata
-      - run: make test-pgsql-migration test-pgsql
+      - name: run migration tests
+        run: make test-pgsql-migration
+      - name: run tests
+        run: make test-pgsql
         timeout-minutes: 50
         env:
           TAGS: bindata gogit
@@ -72,7 +75,10 @@ jobs:
       - run: make backend
         env:
           TAGS: bindata gogit sqlite sqlite_unlock_notify
-      - run: make test-sqlite-migration test-sqlite
+      - name: run migration tests
+        run: make test-sqlite-migration
+      - name: run tests
+        run: make test-sqlite
         timeout-minutes: 50
         env:
           TAGS: bindata gogit sqlite sqlite_unlock_notify
@@ -175,8 +181,10 @@ jobs:
       - run: make backend
         env:
           TAGS: bindata
+      - name: run migration tests
+        run: make test-mysql-migration
       - name: run tests
-        run: make test-mysql-migration integration-test-coverage
+        run: make integration-test-coverage
         env:
           TAGS: bindata
           RACE_ENABLED: true
@@ -208,7 +216,9 @@ jobs:
       - run: make backend
         env:
           TAGS: bindata
-      - run: make test-mssql-migration test-mssql
+      - run: make test-mssql-migration
+      - name: run tests
+        run: make test-mssql
         timeout-minutes: 50
         env:
           TAGS: bindata
diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml
index edd2f6d16e..812819b599 100644
--- a/.github/workflows/pull-labeler.yml
+++ b/.github/workflows/pull-labeler.yml
@@ -9,12 +9,12 @@ concurrency:
   cancel-in-progress: true
 
 jobs:
-  label:
+  labeler:
     runs-on: ubuntu-latest
     permissions:
       contents: read
       pull-requests: write
     steps:
-      - uses: actions/labeler@v4
+      - uses: actions/labeler@v5
         with:
-          dot: true
+          sync-labels: true
diff --git a/.gitignore b/.gitignore
index 814d910315..46c8b9b49c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,7 +15,7 @@ _test
 
 # MS VSCode
 .vscode
-__debug_bin
+__debug_bin*
 
 *.cgo1.go
 *.cgo2.c
@@ -58,7 +58,7 @@ cpu.out
 /data
 /indexers
 /log
-/public/img/avatar
+/public/assets/img/avatar
 /tests/integration/gitea-integration-*
 /tests/integration/indexers-*
 /tests/e2e/gitea-e2e-*
@@ -77,7 +77,6 @@ cpu.out
 /public/assets/css
 /public/assets/fonts
 /public/assets/licenses.txt
-/public/assets/img/webpack
 /vendor
 /web_src/fomantic/node_modules
 /web_src/fomantic/build/*
@@ -95,6 +94,9 @@ cpu.out
 /.air
 /.go-licenses
 
+# Files and folders that were previously generated
+/public/assets/img/webpack
+
 # Snapcraft
 /gitea_a*.txt
 snap/.snapcraft/
diff --git a/.gitpod.yml b/.gitpod.yml
index 35b22c45ae..f573d55a76 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -10,10 +10,19 @@ tasks:
   - name: Run backend
     command: |
       gp sync-await setup
-      if [ ! -f custom/conf/app.ini ]
-      then
+
+      # Get the URL and extract the domain
+      url=$(gp url 3000)
+      domain=$(echo $url | awk -F[/:] '{print $4}')
+
+      if [ -f custom/conf/app.ini ]; then
+        sed -i "s|^ROOT_URL =.*|ROOT_URL = ${url}/|" custom/conf/app.ini
+        sed -i "s|^DOMAIN =.*|DOMAIN = ${domain}|" custom/conf/app.ini
+        sed -i "s|^SSH_DOMAIN =.*|SSH_DOMAIN = ${domain}|" custom/conf/app.ini
+        sed -i "s|^NO_REPLY_ADDRESS =.*|SSH_DOMAIN = noreply.${domain}|" custom/conf/app.ini
+      else
         mkdir -p custom/conf/
-        echo -e "[server]\nROOT_URL=$(gp url 3000)/" > custom/conf/app.ini
+        echo -e "[server]\nROOT_URL = ${url}/" > custom/conf/app.ini
         echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini
       fi
       export TAGS="sqlite sqlite_unlock_notify"
@@ -33,7 +42,7 @@ vscode:
     - DavidAnson.vscode-markdownlint
     - Vue.volar
     - ms-azuretools.vscode-docker
-    - zixuanchen.vitest-explorer
+    - vitest.explorer
     - qwtel.sqlite-viewer
     - GitHub.vscode-pull-request-github
 
diff --git a/.golangci.yml b/.golangci.yml
index d6ce37f49a..5be2cefe44 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -30,10 +30,6 @@ linters:
 
 run:
   timeout: 10m
-  skip-dirs:
-    - node_modules
-    - public
-    - web_src
 
 linters-settings:
   stylecheck:
@@ -94,6 +90,7 @@ linters-settings:
 issues:
   max-issues-per-linter: 0
   max-same-issues: 0
+  exclude-dirs: [node_modules, public, web_src]
   exclude-rules:
     # Exclude some linters from running on tests files.
     - path: _test\.go
diff --git a/.ignore b/.ignore
index 5c945ab981..5b96dabd38 100644
--- a/.ignore
+++ b/.ignore
@@ -4,6 +4,8 @@
 /modules/options/bindata.go
 /modules/public/bindata.go
 /modules/templates/bindata.go
-/vendor
+/options/gitignore
+/options/license
 /public/assets
+/vendor
 node_modules
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
index f740d1a4d6..b251ff796c 100644
--- a/.markdownlint.yaml
+++ b/.markdownlint.yaml
@@ -5,13 +5,11 @@ heading-increment: false
 line-length: {code_blocks: false, tables: false, stern: true, line_length: -1}
 no-alt-text: false
 no-bare-urls: false
-no-blanks-blockquote: false
 no-emphasis-as-heading: false
 no-empty-links: false
 no-hard-tabs: {code_blocks: false}
 no-inline-html: false
 no-space-in-code: false
 no-space-in-emphasis: false
-no-trailing-punctuation: false
 no-trailing-spaces: {br_spaces: 0}
 single-h1: false
diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml
deleted file mode 100644
index a44294ee76..0000000000
--- a/.stylelintrc.yaml
+++ /dev/null
@@ -1,221 +0,0 @@
-plugins:
-  - stylelint-declaration-strict-value
-  - stylelint-declaration-block-no-ignored-properties
-  - "@stylistic/stylelint-plugin"
-
-ignoreFiles:
-  - "**/*.go"
-
-overrides:
-  - files: ["**/chroma/*", "**/codemirror/*", "**/standalone/*", "**/console.css", "font_i18n.css"]
-    rules:
-      scale-unlimited/declaration-strict-value: null
-  - files: ["**/chroma/*", "**/codemirror/*"]
-    rules:
-      block-no-empty: null
-  - files: ["**/*.vue"]
-    customSyntax: postcss-html
-
-rules:
-  "@stylistic/at-rule-name-case": null
-  "@stylistic/at-rule-name-newline-after": null
-  "@stylistic/at-rule-name-space-after": null
-  "@stylistic/at-rule-semicolon-newline-after": null
-  "@stylistic/at-rule-semicolon-space-before": null
-  "@stylistic/block-closing-brace-empty-line-before": null
-  "@stylistic/block-closing-brace-newline-after": null
-  "@stylistic/block-closing-brace-newline-before": null
-  "@stylistic/block-closing-brace-space-after": null
-  "@stylistic/block-closing-brace-space-before": null
-  "@stylistic/block-opening-brace-newline-after": null
-  "@stylistic/block-opening-brace-newline-before": null
-  "@stylistic/block-opening-brace-space-after": null
-  "@stylistic/block-opening-brace-space-before": null
-  "@stylistic/color-hex-case": lower
-  "@stylistic/declaration-bang-space-after": never
-  "@stylistic/declaration-bang-space-before": null
-  "@stylistic/declaration-block-semicolon-newline-after": null
-  "@stylistic/declaration-block-semicolon-newline-before": null
-  "@stylistic/declaration-block-semicolon-space-after": null
-  "@stylistic/declaration-block-semicolon-space-before": never
-  "@stylistic/declaration-block-trailing-semicolon": null
-  "@stylistic/declaration-colon-newline-after": null
-  "@stylistic/declaration-colon-space-after": null
-  "@stylistic/declaration-colon-space-before": never
-  "@stylistic/function-comma-newline-after": null
-  "@stylistic/function-comma-newline-before": null
-  "@stylistic/function-comma-space-after": null
-  "@stylistic/function-comma-space-before": null
-  "@stylistic/function-max-empty-lines": 0
-  "@stylistic/function-parentheses-newline-inside": never-multi-line
-  "@stylistic/function-parentheses-space-inside": null
-  "@stylistic/function-whitespace-after": null
-  "@stylistic/indentation": 2
-  "@stylistic/linebreaks": null
-  "@stylistic/max-empty-lines": 1
-  "@stylistic/max-line-length": null
-  "@stylistic/media-feature-colon-space-after": null
-  "@stylistic/media-feature-colon-space-before": never
-  "@stylistic/media-feature-name-case": null
-  "@stylistic/media-feature-parentheses-space-inside": null
-  "@stylistic/media-feature-range-operator-space-after": always
-  "@stylistic/media-feature-range-operator-space-before": always
-  "@stylistic/media-query-list-comma-newline-after": null
-  "@stylistic/media-query-list-comma-newline-before": null
-  "@stylistic/media-query-list-comma-space-after": null
-  "@stylistic/media-query-list-comma-space-before": null
-  "@stylistic/no-empty-first-line": null
-  "@stylistic/no-eol-whitespace": true
-  "@stylistic/no-extra-semicolons": true
-  "@stylistic/no-missing-end-of-source-newline": null
-  "@stylistic/number-leading-zero": null
-  "@stylistic/number-no-trailing-zeros": null
-  "@stylistic/property-case": lower
-  "@stylistic/selector-attribute-brackets-space-inside": null
-  "@stylistic/selector-attribute-operator-space-after": null
-  "@stylistic/selector-attribute-operator-space-before": null
-  "@stylistic/selector-combinator-space-after": null
-  "@stylistic/selector-combinator-space-before": null
-  "@stylistic/selector-descendant-combinator-no-non-space": null
-  "@stylistic/selector-list-comma-newline-after": null
-  "@stylistic/selector-list-comma-newline-before": null
-  "@stylistic/selector-list-comma-space-after": always-single-line
-  "@stylistic/selector-list-comma-space-before": never-single-line
-  "@stylistic/selector-max-empty-lines": 0
-  "@stylistic/selector-pseudo-class-case": lower
-  "@stylistic/selector-pseudo-class-parentheses-space-inside": never
-  "@stylistic/selector-pseudo-element-case": lower
-  "@stylistic/string-quotes": double
-  "@stylistic/unicode-bom": null
-  "@stylistic/unit-case": lower
-  "@stylistic/value-list-comma-newline-after": null
-  "@stylistic/value-list-comma-newline-before": null
-  "@stylistic/value-list-comma-space-after": null
-  "@stylistic/value-list-comma-space-before": null
-  "@stylistic/value-list-max-empty-lines": 0
-  alpha-value-notation: null
-  annotation-no-unknown: true
-  at-rule-allowed-list: null
-  at-rule-disallowed-list: null
-  at-rule-empty-line-before: null
-  at-rule-no-unknown: true
-  at-rule-no-vendor-prefix: true
-  at-rule-property-required-list: null
-  block-no-empty: true
-  color-function-notation: null
-  color-hex-alpha: null
-  color-hex-length: null
-  color-named: null
-  color-no-hex: null
-  color-no-invalid-hex: true
-  comment-empty-line-before: null
-  comment-no-empty: true
-  comment-pattern: null
-  comment-whitespace-inside: null
-  comment-word-disallowed-list: null
-  custom-media-pattern: null
-  custom-property-empty-line-before: null
-  custom-property-no-missing-var-function: true
-  custom-property-pattern: null
-  declaration-block-no-duplicate-custom-properties: true
-  declaration-block-no-duplicate-properties: [true, {ignore: [consecutive-duplicates-with-different-values]}]
-  declaration-block-no-redundant-longhand-properties: null
-  declaration-block-no-shorthand-property-overrides: null
-  declaration-block-single-line-max-declarations: null
-  declaration-empty-line-before: null
-  declaration-no-important: null
-  declaration-property-max-values: null
-  declaration-property-unit-allowed-list: null
-  declaration-property-unit-disallowed-list: {line-height: [em]}
-  declaration-property-value-allowed-list: null
-  declaration-property-value-disallowed-list: null
-  declaration-property-value-no-unknown: true
-  font-family-name-quotes: always-where-recommended
-  font-family-no-duplicate-names: true
-  font-family-no-missing-generic-family-keyword: true
-  font-weight-notation: null
-  function-allowed-list: null
-  function-calc-no-unspaced-operator: true
-  function-disallowed-list: null
-  function-linear-gradient-no-nonstandard-direction: true
-  function-name-case: lower
-  function-no-unknown: null
-  function-url-no-scheme-relative: null
-  function-url-quotes: always
-  function-url-scheme-allowed-list: null
-  function-url-scheme-disallowed-list: null
-  hue-degree-notation: null
-  import-notation: string
-  keyframe-block-no-duplicate-selectors: true
-  keyframe-declaration-no-important: true
-  keyframe-selector-notation: null
-  keyframes-name-pattern: null
-  length-zero-no-unit: [true, ignore: [custom-properties], ignoreFunctions: [var]]
-  max-nesting-depth: null
-  media-feature-name-allowed-list: null
-  media-feature-name-disallowed-list: null
-  media-feature-name-no-unknown: true
-  media-feature-name-no-vendor-prefix: true
-  media-feature-name-unit-allowed-list: null
-  media-feature-name-value-allowed-list: null
-  media-feature-name-value-no-unknown: true
-  media-feature-range-notation: null
-  media-query-no-invalid: true
-  named-grid-areas-no-invalid: true
-  no-descending-specificity: null
-  no-duplicate-at-import-rules: true
-  no-duplicate-selectors: true
-  no-empty-source: true
-  no-invalid-double-slash-comments: true
-  no-invalid-position-at-import-rule: null
-  no-irregular-whitespace: true
-  no-unknown-animations: null
-  no-unknown-custom-properties: null
-  number-max-precision: null
-  plugin/declaration-block-no-ignored-properties: true
-  property-allowed-list: null
-  property-disallowed-list: null
-  property-no-unknown: true
-  property-no-vendor-prefix: null
-  rule-empty-line-before: null
-  rule-selector-property-disallowed-list: null
-  scale-unlimited/declaration-strict-value: [[/color$/, font-weight], {ignoreValues: /^(inherit|transparent|unset|initial|currentcolor|none)$/, ignoreFunctions: false, disableFix: true, expandShorthand: true}]
-  selector-attribute-name-disallowed-list: null
-  selector-attribute-operator-allowed-list: null
-  selector-attribute-operator-disallowed-list: null
-  selector-attribute-quotes: always
-  selector-class-pattern: null
-  selector-combinator-allowed-list: null
-  selector-combinator-disallowed-list: null
-  selector-disallowed-list: null
-  selector-id-pattern: null
-  selector-max-attribute: null
-  selector-max-class: null
-  selector-max-combinators: null
-  selector-max-compound-selectors: null
-  selector-max-id: null
-  selector-max-pseudo-class: null
-  selector-max-specificity: null
-  selector-max-type: null
-  selector-max-universal: null
-  selector-nested-pattern: null
-  selector-no-qualifying-type: null
-  selector-no-vendor-prefix: true
-  selector-not-notation: null
-  selector-pseudo-class-allowed-list: null
-  selector-pseudo-class-disallowed-list: null
-  selector-pseudo-class-no-unknown: true
-  selector-pseudo-element-allowed-list: null
-  selector-pseudo-element-colon-notation: double
-  selector-pseudo-element-disallowed-list: null
-  selector-pseudo-element-no-unknown: true
-  selector-type-case: lower
-  selector-type-no-unknown: [true, {ignore: [custom-elements]}]
-  shorthand-property-no-redundant-values: true
-  string-no-newline: true
-  time-min-milliseconds: null
-  unit-allowed-list: null
-  unit-disallowed-list: null
-  unit-no-unknown: true
-  value-keyword-case: null
-  value-no-vendor-prefix: [true, {ignoreValues: [box, inline-box]}]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae87638f1c..e119d0bec0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,240 @@ This changelog goes through all the changes that have been made in each release
 without substantial changes to our git log; to see the highlights of what has
 been added to each release, please refer to the [blog](https://blog.gitea.com).
 
+## [1.21.6](https://github.com/go-gitea/gitea/releases/tag/v1.21.6) - 2024-02-22
+
+* SECURITY
+  * Fix XSS vulnerabilities (#29336)
+  * Use general token signing secret (#29205) (#29325)
+* ENHANCEMENTS
+  * Refactor git version functions and check compatibility (#29155) (#29157)
+  * Improve user experience for outdated comments (#29050) (#29086)
+  * Hide code links on release page if user cannot read code (#29064) (#29066)
+  * Wrap contained tags and branches again (#29021) (#29026)
+  * Fix incorrect button CSS usages (#29015) (#29023)
+  * Strip trailing newline in markdown code copy (#29019) (#29022)
+  * Implement some action notifier functions (#29173) (#29308)
+  * Load outdated comments when (un)resolving conversation on PR timeline (#29203) (#29221)
+* BUGFIXES
+  * Refactor issue template parsing and fix API endpoint (#29069) (#29140)
+  * Fix swift packages not resolving (#29095) (#29102)
+  * Remove SSH workaround (#27893) (#29332)
+  * Only log error when tag sync fails (#29295) (#29327)
+  * Fix SSPI user creation (#28948) (#29323)
+  * Improve the `issue_comment` workflow trigger event (#29277) (#29322)
+  * Discard unread data of `git cat-file` (#29297) (#29310)
+  * Fix error display when merging PRs (#29288) (#29309)
+  * Prevent double use of `git cat-file` session. (#29298) (#29301)
+  * Fix missing link on outgoing new release notifications (#29079) (#29300)
+  * Fix debian InRelease Acquire-By-Hash newline (#29204) (#29299)
+  * Always write proc-receive hook for all git versions (#29287) (#29291)
+  * Do not show delete button when time tracker is disabled (#29257) (#29279)
+  * Workaround to clean up old reviews on creating a new one (#28554) (#29264)
+  * Fix bug when the linked account was disactived and list the linked accounts (#29263)
+  * Do not use lower tag names to find releases/tags (#29261) (#29262)
+  * Fix missed edit issues event for actions (#29237) (#29251)
+  * Only delete scheduled workflows when needed (#29091) (#29235)
+  * Make submit event code work with both jQuery event and native event (#29223) (#29234)
+  * Fix push to create with capitalize repo name (#29090) (#29206)
+  * Use ghost user if user was not found (#29161) (#29169)
+  * Dont load Review if Comment is CommentTypeReviewRequest (#28551) (#29160)
+  * Refactor parseSignatureFromCommitLine (#29054) (#29108)
+  * Avoid showing unnecessary JS errors when there are elements with different origin on the page (#29081) (#29089)
+  * Fix gitea-origin-url with default ports (#29085) (#29088)
+  * Fix orgmode link resolving (#29024) (#29076)
+  * Fix Elasticsearh Request Entity Too Large #28117 (#29062) (#29075)
+  * Do not render empty comments (#29039) (#29049)
+  * Avoid sending update/delete release notice when it is draft (#29008) (#29025)
+  * Fix gitea-action user avatar broken on edited menu (#29190) (#29307)
+  * Disallow merge when required checked are missing (#29143) (#29268)
+  * Fix incorrect link to swift doc and swift package-registry login command (#29096) (#29103)
+  * Convert visibility to number (#29226) (#29244)
+* DOCS
+  * Remove outdated docs from some languages (#27530) (#29208)
+  * Fix typos in the documentation (#29048) (#29056)
+  * Explained where create issue/PR template (#29035)
+
+## [1.21.5](https://github.com/go-gitea/gitea/releases/tag/v1.21.5) - 2024-01-31
+
+* SECURITY
+  * Prevent anonymous container access if `RequireSignInView` is enabled (#28877) (#28882)
+  * Update go dependencies and fix go-git (#28893) (#28934)
+* BUGFIXES
+  * Revert "Speed up loading the dashboard on mysql/mariadb (#28546)" (#29006) (#29007)
+  * Fix an actions schedule bug (#28942) (#28999)
+  * Fix update enable_prune even if mirror_interval is not provided (#28905) (#28929)
+  * Fix uploaded artifacts should be overwritten (#28726) backport v1.21 (#28832)
+  * Preserve BOM in web editor (#28935) (#28959)
+  * Strip `/` from relative links (#28932) (#28952)
+  * Don't remove all mirror repository's releases when mirroring (#28817) (#28939)
+  * Implement `MigrateRepository` for the actions notifier (#28920) (#28923)
+  * Respect branch info for relative links (#28909) (#28922)
+  * Don't reload timeline page when (un)resolving or replying conversation (#28654) (#28917)
+  * Only migrate the first 255 chars of a Github issue title (#28902) (#28912)
+  * Fix sort bug on repository issues list (#28897) (#28901)
+  * Fix `DeleteCollaboration` transaction behaviour (#28886) (#28889)
+  * Fix schedule not trigger bug because matching full ref name with short ref name (#28874) (#28888)
+  * Fix migrate storage bug (#28830) (#28867)
+  * Fix archive creating LFS hooks and breaking pull requests (#28848) (#28851)
+  * Fix reverting a merge commit failing (#28794) (#28825)
+  * Upgrade xorm to v1.3.7 to fix a resource leak problem caused by Iterate (#28891) (#28895)
+  * Fix incorrect PostgreSQL connection string for Unix sockets (#28865) (#28870)
+* ENHANCEMENTS
+  * Make loading animation less aggressive (#28955) (#28956)
+  * Avoid duplicate JS error messages on UI (#28873) (#28881)
+  * Bump `@github/relative-time-element` to 4.3.1 (#28819) (#28826)
+* MISC
+  * Warn that `DISABLE_QUERY_AUTH_TOKEN` is false only if it's explicitly defined (#28783) (#28868)
+  * Remove duplicated checkinit on git module (#28824) (#28831)
+
+## [1.21.4](https://github.com/go-gitea/gitea/releases/tag/v1.21.4) - 2024-01-16
+
+* SECURITY
+  * Update github.com/cloudflare/circl (#28789) (#28790)
+  * Require token for GET subscription endpoint (#28765) (#28768)
+* BUGFIXES
+  * Use refname:strip-2 instead of refname:short when syncing tags (#28797) (#28811)
+  * Fix links in issue card (#28806) (#28807)
+  * Fix nil pointer panic when exec some gitea cli command (#28791) (#28795)
+  * Require token for GET subscription endpoint (#28765) (#28778)
+  * Fix button size in "attached header right" (#28770) (#28774)
+  * Fix `convert.ToTeams` on empty input (#28426) (#28767)
+  * Hide code related setting options in repository when code unit is disabled (#28631) (#28749)
+  * Fix incorrect URL for "Reference in New Issue" (#28716) (#28723)
+  * Fix panic when parsing empty pgsql host (#28708) (#28709)
+  * Upgrade xorm to new version which supported update join for all supported databases (#28590) (#28668)
+  * Fix alpine package files are not rebuilt (#28638) (#28665)
+  * Avoid cycle-redirecting user/login page (#28636) (#28658)
+  * Fix empty ref for cron workflow runs (#28640) (#28647)
+  * Remove unnecessary syncbranchToDB with tests (#28624) (#28629)
+  * Use known issue IID to generate new PR index number when migrating from GitLab (#28616) (#28618)
+  * Fix flex container width (#28603) (#28605)
+  * Fix the scroll behavior for emoji/mention list (#28597) (#28601)
+  * Fix wrong due date rendering in issue list page (#28588) (#28591)
+  * Fix `status_check_contexts` matching bug (#28582) (#28589)
+  * Fix 500 error of searching commits (#28576) (#28579)
+  * Use information from previous blame parts (#28572) (#28577)
+  * Update mermaid for 1.21 (#28571)
+  * Fix 405 method not allowed CORS / OIDC (#28583) (#28586) (#28587) (#28611)
+  * Fix `GetCommitStatuses` (#28787) (#28804)
+  * Forbid removing the last admin user (#28337) (#28793)
+  * Fix schedule tasks bugs (#28691) (#28780)
+  * Fix issue dependencies (#27736) (#28776)
+  * Fix system webhooks API bug (#28531) (#28666)
+  * Fix when private user following user, private user will not be counted in his own view (#28037) (#28792)
+  * Render code block in activity tab (#28816) (#28818)
+* ENHANCEMENTS
+  * Rework markup link rendering (#26745) (#28803)
+  * Modernize merge button (#28140) (#28786)
+  * Speed up loading the dashboard on mysql/mariadb (#28546) (#28784)
+  * Assign pull request to project during creation (#28227) (#28775)
+  * Show description as tooltip instead of title for labels (#28754) (#28766)
+  * Make template `DateTime` show proper tooltip (#28677) (#28683)
+  * Switch destination directory for apt signing keys (#28639) (#28642)
+  * Include heap pprof in diagnosis report to help debugging memory leaks (#28596) (#28599)
+* DOCS
+  * Suggest to use Type=simple for systemd service (#28717) (#28722)
+  * Extend description for ARTIFACT_RETENTION_DAYS (#28626) (#28630)
+* MISC
+  * Add -F to commit search to treat keywords as strings (#28744) (#28748)
+  * Add download attribute to release attachments (#28739) (#28740)
+  * Concatenate error in `checkIfPRContentChanged` (#28731) (#28737)
+  * Improve 1.21 document for Database Preparation (#28643) (#28644)
+
+## [1.21.3](https://github.com/go-gitea/gitea/releases/tag/v1.21.3) - 2023-12-21
+
+* SECURITY
+  * Update golang.org/x/crypto (#28519)
+* API
+  * chore(api): support ignore password if login source type is LDAP for creating user API (#28491) (#28525)
+  * Add endpoint for not implemented Docker auth (#28457) (#28462)
+* ENHANCEMENTS
+  * Add option to disable ambiguous unicode characters detection (#28454) (#28499)
+  * Refactor SSH clone URL generation code (#28421) (#28480)
+  * Polyfill SubmitEvent for PaleMoon (#28441) (#28478)
+* BUGFIXES
+  * Fix the issue ref rendering for wiki (#28556) (#28559)
+  * Fix duplicate ID when deleting repo (#28520) (#28528)
+  * Only check online runner when detecting matching runners in workflows (#28286) (#28512)
+  * Initalize stroage for orphaned repository doctor (#28487) (#28490)
+  * Fix possible nil pointer access (#28428) (#28440)
+  * Don't show unnecessary citation JS error on UI (#28433) (#28437)
+* DOCS
+  * Update actions document about comparsion as Github Actions (#28560) (#28564)
+  * Fix documents for "custom/public/assets/" (#28465) (#28467)
+* MISC
+  * Fix inperformant query on retrifing review from database. (#28552) (#28562)
+  * Improve the prompt for "ssh-keygen sign" (#28509) (#28510)
+  * Update docs for DISABLE_QUERY_AUTH_TOKEN (#28485) (#28488)
+  * Fix Chinese translation of config cheat sheet[API] (#28472) (#28473)
+  * Retry SSH key verification with additional CRLF if it failed (#28392) (#28464)
+
+## [1.21.2](https://github.com/go-gitea/gitea/releases/tag/v1.21.2) - 2023-12-12
+
+* SECURITY
+  * Rebuild with recently released golang version
+  * Fix missing check (#28406) (#28411)
+  * Do some missing checks (#28423) (#28432)
+* BUGFIXES
+  * Fix margin in server signed signature verification view (#28379) (#28381)
+  * Fix object does not exist error when checking citation file (#28314) (#28369)
+  * Use `filepath` instead of `path` to create SQLite3 database file (#28374) (#28378)
+  * Fix the runs will not be displayed bug when the main branch have no workflows but other branches have (#28359) (#28365)
+  * Handle repository.size column being NULL in migration v263 (#28336) (#28363)
+  * Convert git commit summary to valid UTF8. (#28356) (#28358)
+  * Fix migration panic due to an empty review comment diff (#28334) (#28362)
+  * Add `HEAD` support for rpm repo files (#28309) (#28360)
+  * Fix RPM/Debian signature key creation (#28352) (#28353)
+  * Keep profile tab when clicking on Language (#28320) (#28331)
+  * Fix missing issue search index update when changing status (#28325) (#28330)
+  * Fix wrong link in `protect_branch_name_pattern_desc` (#28313) (#28315)
+  * Read `previous` info from git blame (#28306) (#28310)
+  * Ignore "non-existing" errors when getDirectorySize calculates the size (#28276) (#28285)
+  * Use appSubUrl for OAuth2 callback URL tip (#28266) (#28275)
+  * Meilisearch: require all query terms to be matched (#28293) (#28296)
+  * Fix required error for token name (#28267) (#28284)
+  * Fix issue will be detected as pull request when checking `First-time contributor` (#28237) (#28271)
+  * Use full width for project boards (#28225) (#28245)
+  * Increase "version" when update the setting value to a same value as before (#28243) (#28244)
+  * Also sync DB branches on push if necessary (#28361) (#28403)
+  * Make gogit Repository.GetBranchNames consistent (#28348) (#28386)
+  * Recover from panic in cron task (#28409) (#28425)
+  * Deprecate query string auth tokens (#28390) (#28430)
+* ENHANCEMENTS
+  * Improve doctor cli behavior (#28422) (#28424)
+  * Fix margin in server signed signature verification view (#28379) (#28381)
+  * Refactor template empty checks (#28351) (#28354)
+  * Read `previous` info from git blame (#28306) (#28310)
+  * Use full width for project boards (#28225) (#28245)
+  * Enable system users search via the API (#28013) (#28018)
+
+## [1.21.1](https://github.com/go-gitea/gitea/releases/tag/v1.21.1) - 2023-11-26
+
+* SECURITY
+  * Fix comment permissions (#28213) (#28216)
+* BUGFIXES
+  * Fix delete-orphaned-repos (#28200) (#28202)
+  * Make CORS work for oauth2 handlers (#28184) (#28185)
+  * Fix missing buttons (#28179) (#28181)
+  * Fix no ActionTaskOutput table waring (#28149) (#28152)
+  * Fix empty action run title (#28113) (#28148)
+  * Use "is-loading" to avoid duplicate form submit for code comment (#28143) (#28147)
+  * Fix Matrix and MSTeams nil dereference (#28089) (#28105)
+  * Fix incorrect pgsql conn builder behavior (#28085) (#28098)
+  * Fix system config cache expiration timing (#28072) (#28090)
+  * Restricted users only see repos in orgs which their team was assigned to (#28025) (#28051)
+* API
+  * Fix permissions for Token DELETE endpoint to match GET and POST (#27610) (#28099)
+* ENHANCEMENTS
+  * Do not display search box when there's no packages yet (#28146) (#28159)
+  * Add missing `packages.cleanup.success` (#28129) (#28132)
+* DOCS
+  * Docs: Replace deprecated IS_TLS_ENABLED mailer setting in email setup (#28205) (#28208)
+  * Fix the description about the default setting for action in quick start document (#28160) (#28168)
+  * Add guide page to actions when there's no workflows (#28145) (#28153)
+* MISC
+  * Use full width for PR comparison (#28182) (#28186)
+
 ## [1.21.0](https://github.com/go-gitea/gitea/releases/tag/v1.21.0) - 2023-11-14
 
 * BREAKING
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 96b02edd5b..5d20bc2589 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,6 +8,7 @@
     - [How to report issues](#how-to-report-issues)
     - [Types of issues](#types-of-issues)
     - [Discuss your design before the implementation](#discuss-your-design-before-the-implementation)
+    - [Issue locking](#issue-locking)
   - [Building Gitea](#building-gitea)
   - [Dependencies](#dependencies)
     - [Backend](#backend)
@@ -47,6 +48,7 @@
   - [Release Cycle](#release-cycle)
   - [Maintainers](#maintainers)
   - [Technical Oversight Committee (TOC)](#technical-oversight-committee-toc)
+    - [TOC election process](#toc-election-process)
     - [Current TOC members](#current-toc-members)
     - [Previous TOC/owners members](#previous-tocowners-members)
   - [Governance Compensation](#governance-compensation)
@@ -102,6 +104,13 @@ the goals for the project and tools.
 
 Pull requests should not be the place for architecture discussions.
 
+### Issue locking
+
+Commenting on closed or merged issues/PRs is strongly discouraged.
+Such comments will likely be overlooked as some maintainers may not view notifications on closed issues, thinking that the item is resolved.
+As such, commenting on closed/merged issues/PRs may be disabled prior to the scheduled auto-locking if a discussion starts or if unrelated comments are posted.
+If further discussion is needed, we encourage you to open a new issue instead and we recommend linking to the issue/PR in question for context.
+
 ## Building Gitea
 
 See the [development setup instructions](https://docs.gitea.com/development/hacking-on-gitea).
@@ -455,7 +464,7 @@ We assume in good faith that the information you provide is legally binding.
 We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. \
 The overall goal is to make a major release every three or four months, which breaks down into two or three months of general development followed by one month of testing and polishing known as the release freeze. \
 All the feature pull requests should be
-merged before feature freeze. And, during the frozen period, a corresponding
+merged before feature freeze. All feature pull requests haven't been merged before this feature freeze will be moved to next milestone, please notice our feature freeze announcement on discord. And, during the frozen period, a corresponding
 release branch is open for fixes backported from main branch. Release candidates
 are made during this period for user testing to
 obtain a final version that is maintained in this branch.
@@ -486,36 +495,53 @@ if possible provide GPG signed commits.
 https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
 https://help.github.com/articles/signing-commits-with-gpg/
 
+Furthermore, any account with write access (like bots and TOC members) **must** use 2FA.
+https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
+
 ## Technical Oversight Committee (TOC)
 
-At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions would be elected as it has been over the past years, and the other three would consist of appointed members from the Gitea company.
+At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company.
 https://blog.gitea.com/quarterly-23q1/
 
-When the new community members have been elected, the old members will give up ownership to the newly elected members. For security reasons, TOC members or any account with write access (like a bot) must use 2FA.
-https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
+### TOC election process
+
+Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company.
+A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election.
+If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate.
+
+The TOC is elected for one year, the TOC election happens yearly.
+After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat.
+If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat.
+Refusals result in the person with the next highest vote getting the same choice.
+As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat.
+
+If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration.
 
 ### Current TOC members
 
-- 2023-01-01 ~ 2023-12-31 - https://blog.gitea.com/quarterly-23q1/
+- 2024-01-01 ~ 2024-12-31
   - Company
     - [Jason Song](https://gitea.com/wolfogre) <i@wolfogre.com>
     - [Lunny Xiao](https://gitea.com/lunny) <xiaolunwen@gmail.com>
-    - [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.io>
+    - [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.com>
   - Community
     - [6543](https://gitea.com/6543) <6543@obermui.de>
-    - [Andrew Thornton](https://gitea.com/zeripath) <art27@cantab.net>
+    - [delvh](https://gitea.com/delvh) <dev.lh@web.de>
     - [John Olheiser](https://gitea.com/jolheiser) <john.olheiser@gmail.com>
 
 ### Previous TOC/owners members
 
 Here's the history of the owners and the time they served:
 
-- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872)
+- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
 - [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017
 - [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017
 - [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801)
-- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872)
-- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872)
+- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
+- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
+- [6543](https://gitea.com/6543) - 2023
+- [John Olheiser](https://gitea.com/jolheiser) - 2023
+- [Jason Song](https://gitea.com/wolfogre) - 2023
 
 ## Governance Compensation
 
diff --git a/Dockerfile b/Dockerfile
index 325b0255df..b647c0cd59 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # Build stage
-FROM docker.io/library/golang:1.21-alpine3.19 AS build-env
+FROM docker.io/library/golang:1.22-alpine3.19 AS build-env
 
 ARG GOPROXY
 ENV GOPROXY ${GOPROXY:-direct}
diff --git a/Dockerfile.rootless b/Dockerfile.rootless
index 6f27c698ac..dd7da97278 100644
--- a/Dockerfile.rootless
+++ b/Dockerfile.rootless
@@ -1,5 +1,5 @@
 # Build stage
-FROM docker.io/library/golang:1.21-alpine3.19 AS build-env
+FROM docker.io/library/golang:1.22-alpine3.19 AS build-env
 
 ARG GOPROXY
 ENV GOPROXY ${GOPROXY:-direct}
diff --git a/MAINTAINERS b/MAINTAINERS
index 72171f80ed..eed87529a3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -59,3 +59,5 @@ Rui Chen  <rui@chenrui.dev> (@chenrui333)
 Nanguan Lin <nanguanlin6@gmail.com> (@lng2020)
 kerwin612 <kerwin612@qq.com> (@kerwin612)
 Gary Wang <git@blumia.net> (@BLumia)
+Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis)
+Yu Liu <1240335630@qq.com> (@HEREYUA)
diff --git a/Makefile b/Makefile
index 273ae1fa68..f1acfbc81e 100644
--- a/Makefile
+++ b/Makefile
@@ -23,28 +23,25 @@ SHASUM ?= shasum -a 256
 HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
 COMMA := ,
 
-XGO_VERSION := go-1.21.x
+XGO_VERSION := go-1.22.x
 
-AIR_PACKAGE ?= github.com/cosmtrek/air@v1.49.0
+AIR_PACKAGE ?= github.com/cosmtrek/air@v1
 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0
 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0
-GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2
+GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2
 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
-MISSPELL_PACKAGE ?= github.com/client9/misspell/cmd/misspell@v0.3.4
-SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.5
+MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1
+SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@db51e79a0e37c572d8b59ae0c58bf2bbbbe53285
 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
-GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0
-GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.0.3
-ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.6.26
+GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
+GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
+ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
 
 DOCKER_IMAGE ?= gitea/gitea
 DOCKER_TAG ?= latest
 DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
 
 ifeq ($(HAS_GO), yes)
-	GOPATH ?= $(shell $(GO) env GOPATH)
-	export PATH := $(GOPATH)/bin:$(PATH)
-
 	CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
 	CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
 endif
@@ -115,13 +112,14 @@ LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
 
 GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
 GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
+MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
 
 FOMANTIC_WORK_DIR := web_src/fomantic
 
 WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
-WEBPACK_CONFIGS := webpack.config.js
+WEBPACK_CONFIGS := webpack.config.js tailwind.config.js
 WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
-WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts public/assets/img/webpack
+WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts
 
 BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
 BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST))
@@ -146,6 +144,11 @@ TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMAN
 GO_DIRS := build cmd models modules routers services tests
 WEB_DIRS := web_src/js web_src/css
 
+ESLINT_FILES := web_src/js tools *.config.js tests/e2e
+STYLELINT_FILES := web_src/css web_src/js/components/*.vue
+SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github
+EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
+
 GO_SOURCES := $(wildcard *.go)
 GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go)
 GO_SOURCES += $(GENERATED_GO_DEST)
@@ -162,8 +165,8 @@ ifdef DEPS_PLAYWRIGHT
 endif
 
 SWAGGER_SPEC := templates/swagger/v1_json.tmpl
-SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|g
-SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|"basePath": "/api/v1"|g
+SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape}}/api/v1"|g
+SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape}}/api/v1"|"basePath": "/api/v1"|g
 SWAGGER_EXCLUDE := code.gitea.io/sdk
 SWAGGER_NEWLINE_COMMAND := -e '$$a\'
 
@@ -219,6 +222,8 @@ help:
 	@echo " - lint-swagger                     lint swagger files"
 	@echo " - lint-templates                   lint template files"
 	@echo " - lint-yaml                        lint yaml files"
+	@echo " - lint-spell                       lint spelling"
+	@echo " - lint-spell-fix                   lint spelling and fix issues"
 	@echo " - checks                           run various consistency checks"
 	@echo " - checks-frontend                  check frontend files"
 	@echo " - checks-backend                   check backend files"
@@ -308,10 +313,6 @@ fmt-check: fmt
 	  exit 1; \
 	fi
 
-.PHONY: misspell-check
-misspell-check:
-	go run $(MISSPELL_PACKAGE) -error $(GO_DIRS) $(WEB_DIRS)
-
 .PHONY: $(TAGS_EVIDENCE)
 $(TAGS_EVIDENCE):
 	@mkdir -p $(MAKE_EVIDENCE_DIR)
@@ -351,13 +352,13 @@ checks: checks-frontend checks-backend
 checks-frontend: lockfile-check svg-check
 
 .PHONY: checks-backend
-checks-backend: tidy-check swagger-check fmt-check misspell-check swagger-validate security-check
+checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check
 
 .PHONY: lint
-lint: lint-frontend lint-backend
+lint: lint-frontend lint-backend lint-spell
 
 .PHONY: lint-fix
-lint-fix: lint-frontend-fix lint-backend-fix
+lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix
 
 .PHONY: lint-frontend
 lint-frontend: lint-js lint-css
@@ -373,19 +374,19 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
 
 .PHONY: lint-js
 lint-js: node_modules
-	npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js tests/e2e
+	npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES)
 
 .PHONY: lint-js-fix
 lint-js-fix: node_modules
-	npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js tests/e2e --fix
+	npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES) --fix
 
 .PHONY: lint-css
 lint-css: node_modules
-	npx stylelint --color --max-warnings=0 web_src/css web_src/js/components/*.vue
+	npx stylelint --color --max-warnings=0 $(STYLELINT_FILES)
 
 .PHONY: lint-css-fix
 lint-css-fix: node_modules
-	npx stylelint --color --max-warnings=0 web_src/css web_src/js/components/*.vue --fix
+	npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix
 
 .PHONY: lint-swagger
 lint-swagger: node_modules
@@ -395,6 +396,14 @@ lint-swagger: node_modules
 lint-md: node_modules
 	npx markdownlint docs *.md
 
+.PHONY: lint-spell
+lint-spell:
+	@go run $(MISSPELL_PACKAGE) -error $(SPELLCHECK_FILES)
+
+.PHONY: lint-spell-fix
+lint-spell-fix:
+	@go run $(MISSPELL_PACKAGE) -w $(SPELLCHECK_FILES)
+
 .PHONY: lint-go
 lint-go:
 	$(GO) run $(GOLANGCI_LINT_PACKAGE) run
@@ -418,14 +427,15 @@ lint-go-vet:
 
 .PHONY: lint-editorconfig
 lint-editorconfig:
-	$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .github/workflows
+	@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
 
 .PHONY: lint-actions
 lint-actions:
 	$(GO) run $(ACTIONLINT_PACKAGE)
 
 .PHONY: lint-templates
-lint-templates: .venv
+lint-templates: .venv node_modules
+	@node tools/lint-templates-svg.js
 	@poetry run djlint $(shell find templates -type f -iname '*.tmpl')
 
 .PHONY: lint-yaml
@@ -434,7 +444,7 @@ lint-yaml: .venv
 
 .PHONY: watch
 watch:
-	@bash build/watch.sh
+	@bash tools/watch.sh
 
 .PHONY: watch-frontend
 watch-frontend: node-check node_modules
@@ -594,8 +604,7 @@ test-mssql\#%: integrations.mssql.test generate-ini-mssql
 test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test
 
 .PHONY: playwright
-playwright: $(PLAYWRIGHT_DIR)
-	npm install --no-save @playwright/test
+playwright: deps-frontend
 	npx playwright install $(PLAYWRIGHT_FLAGS)
 
 .PHONY: test-e2e%
@@ -702,9 +711,7 @@ migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
 
 .PHONY: migrations.individual.mysql.test
 migrations.individual.mysql.test: $(GO_SOURCES)
-	for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
-		GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
-	done
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
 
 .PHONY: migrations.individual.sqlite.test\#%
 migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
@@ -712,20 +719,15 @@ migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
 
 .PHONY: migrations.individual.pgsql.test
 migrations.individual.pgsql.test: $(GO_SOURCES)
-	for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
-		GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
-	done
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
 
 .PHONY: migrations.individual.pgsql.test\#%
 migrations.individual.pgsql.test\#%: $(GO_SOURCES) generate-ini-pgsql
 	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
 
-
 .PHONY: migrations.individual.mssql.test
 migrations.individual.mssql.test: $(GO_SOURCES) generate-ini-mssql
-	for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
-		GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg -test.failfast; \
-	done
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
 
 .PHONY: migrations.individual.mssql.test\#%
 migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql
@@ -733,9 +735,7 @@ migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql
 
 .PHONY: migrations.individual.sqlite.test
 migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
-	for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
-		GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
-	done
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
 
 .PHONY: migrations.individual.sqlite.test\#%
 migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
@@ -839,10 +839,6 @@ release-sources: | $(DIST_DIRS)
 release-docs: | $(DIST_DIRS) docs
 	tar -czf $(DIST)/release/gitea-docs-$(VERSION).tar.gz -C ./docs .
 
-.PHONY: docs
-docs:
-	cd docs; bash scripts/trans-copy.sh;
-
 .PHONY: deps
 deps: deps-frontend deps-backend deps-tools deps-py
 
@@ -901,6 +897,7 @@ fomantic:
 	cd $(FOMANTIC_WORK_DIR) && npm install --no-save
 	cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config
 	cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/
+	$(SED_INPLACE) -e 's/  overrideBrowserslist\r/  overrideBrowserslist: ["defaults"]\r/g' $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/tasks/config/tasks.js
 	cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build
 	# fomantic uses "touchstart" as click event for some browsers, it's not ideal, so we force fomantic to always use "click" as click event
 	$(SED_INPLACE) -e 's/clickEvent[ \t]*=/clickEvent = "click", unstableClickEvent =/g' $(FOMANTIC_WORK_DIR)/build/semantic.js
@@ -919,7 +916,7 @@ $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json
 .PHONY: svg
 svg: node-check | node_modules
 	rm -rf $(SVG_DEST_DIR)
-	node build/generate-svg.js
+	node tools/generate-svg.js
 
 .PHONY: svg-check
 svg-check: svg
@@ -962,8 +959,8 @@ generate-gitignore:
 
 .PHONY: generate-images
 generate-images: | node_modules
-	npm install --no-save --no-package-lock fabric@5 imagemin-zopfli@7
-	node build/generate-images.js $(TAGS)
+	npm install --no-save fabric@6.0.0-beta20 imagemin-zopfli@7
+	node tools/generate-images.js $(TAGS)
 
 .PHONY: generate-manpage
 generate-manpage:
@@ -980,3 +977,8 @@ docker:
 
 # This endif closes the if at the top of the file
 endif
+
+# Disable parallel execution because it would break some targets that don't
+# specify exact dependencies like 'backend' which does currently not depend
+# on 'frontend' to enable Node.js-less builds from source tarballs.
+.NOTPARALLEL:
diff --git a/README.md b/README.md
index 5356fcfacd..f579449174 100644
--- a/README.md
+++ b/README.md
@@ -1,58 +1,18 @@
-<p align="center">
-  <a href="https://gitea.io/">
-    <img alt="Gitea" src="https://raw.githubusercontent.com/go-gitea/gitea/main/public/assets/img/gitea.svg" width="220"/>
-  </a>
-</p>
-<h1 align="center">Gitea - Git with a cup of tea</h1>
+# Gitea
 
-<p align="center">
-  <a href="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain" title="Release Nightly">
-    <img src="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main">
-  </a>
-  <a href="https://discord.gg/Gitea" title="Join the Discord chat at https://discord.gg/Gitea">
-    <img src="https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2">
-  </a>
-  <a href="https://app.codecov.io/gh/go-gitea/gitea" title="Codecov">
-    <img src="https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg">
-  </a>
-  <a href="https://goreportcard.com/report/code.gitea.io/gitea" title="Go Report Card">
-    <img src="https://goreportcard.com/badge/code.gitea.io/gitea">
-  </a>
-  <a href="https://pkg.go.dev/code.gitea.io/gitea" title="GoDoc">
-    <img src="https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg">
-  </a>
-  <a href="https://github.com/go-gitea/gitea/releases/latest" title="GitHub release">
-    <img src="https://img.shields.io/github/release/go-gitea/gitea.svg">
-  </a>
-  <a href="https://www.codetriage.com/go-gitea/gitea" title="Help Contribute to Open Source">
-    <img src="https://www.codetriage.com/go-gitea/gitea/badges/users.svg">
-  </a>
-  <a href="https://opencollective.com/gitea" title="Become a backer/sponsor of gitea">
-    <img src="https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen">
-  </a>
-  <a href="https://opensource.org/licenses/MIT" title="License: MIT">
-    <img src="https://img.shields.io/badge/License-MIT-blue.svg">
-  </a>
-  <a href="https://gitpod.io/#https://github.com/go-gitea/gitea">
-  <img
-    src="https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod"
-    alt="Contribute with Gitpod"
-  />
-  </a>
-  <a href="https://crowdin.com/project/gitea" title="Crowdin">
-    <img src="https://badges.crowdin.net/gitea/localized.svg">
-  </a>
-  <a href="https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main" title="TODOs">
-    <img src="https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main">
-  </a>
-  <a href="https://app.bountysource.com/teams/gitea" title="Bountysource">
-    <img src="https://img.shields.io/bountysource/team/gitea/activity">
-  </a>
-</p>
+[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
+[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
+[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
+[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
+[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
+[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
+[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
+[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
+[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/go-gitea/gitea)
+[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin")
+[![](https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main)](https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main "TODOs")
 
-<p align="center">
-  <a href="README_ZH.md">View this document in Chinese</a>
-</p>
+[View this document in Chinese](./README_ZH.md)
 
 ## Purpose
 
@@ -89,25 +49,23 @@ The `build` target is split into two sub-targets:
 
 Internet connectivity is required to download the go and npm modules. When building from the official source tarballs which include pre-built frontend files, the `frontend` target will not be triggered, making it possible to build without Node.js.
 
-Parallelism (`make -j <num>`) is not supported.
-
 More info: https://docs.gitea.com/installation/install-from-source
 
 ## Using
 
     ./gitea web
 
-NOTE: If you're interested in using our APIs, we have experimental
-support with [documentation](https://try.gitea.io/api/swagger).
+> [!NOTE]
+> If you're interested in using our APIs, we have experimental support with [documentation](https://try.gitea.io/api/swagger).
 
 ## Contributing
 
 Expected workflow is: Fork -> Patch -> Push -> Pull Request
 
-NOTES:
-
-1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
-2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
+> [!NOTE]
+>
+> 1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
+> 2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
 
 ## Translating
 
diff --git a/README_ZH.md b/README_ZH.md
index 0d9092a0fd..726c4273a6 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -1,58 +1,18 @@
-<p align="center">
-  <a href="https://gitea.io/">
-    <img alt="Gitea" src="https://raw.githubusercontent.com/go-gitea/gitea/main/public/assets/img/gitea.svg" width="220"/>
-  </a>
-</p>
-<h1 align="center">Gitea - Git with a cup of tea</h1>
+# Gitea
 
-<p align="center">
-  <a href="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain" title="Release Nightly">
-    <img src="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main">
-  </a>
-  <a href="https://discord.gg/Gitea" title="Join the Discord chat at https://discord.gg/Gitea">
-    <img src="https://img.shields.io/discord/322538954119184384.svg">
-  </a>
-  <a href="https://app.codecov.io/gh/go-gitea/gitea" title="Codecov">
-    <img src="https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg">
-  </a>
-  <a href="https://goreportcard.com/report/code.gitea.io/gitea" title="Go Report Card">
-    <img src="https://goreportcard.com/badge/code.gitea.io/gitea">
-  </a>
-  <a href="https://pkg.go.dev/code.gitea.io/gitea" title="GoDoc">
-    <img src="https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg">
-  </a>
-  <a href="https://github.com/go-gitea/gitea/releases/latest" title="GitHub release">
-    <img src="https://img.shields.io/github/release/go-gitea/gitea.svg">
-  </a>
-  <a href="https://www.codetriage.com/go-gitea/gitea" title="Help Contribute to Open Source">
-    <img src="https://www.codetriage.com/go-gitea/gitea/badges/users.svg">
-  </a>
-  <a href="https://opencollective.com/gitea" title="Become a backer/sponsor of gitea">
-    <img src="https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen">
-  </a>
-  <a href="https://opensource.org/licenses/MIT" title="License: MIT">
-    <img src="https://img.shields.io/badge/License-MIT-blue.svg">
-  </a>
-  <a href="https://gitpod.io/#https://github.com/go-gitea/gitea">
-  <img
-    src="https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod"
-    alt="Contribute with Gitpod"
-  />
-  </a>
-  <a href="https://crowdin.com/project/gitea" title="Crowdin">
-    <img src="https://badges.crowdin.net/gitea/localized.svg">
-  </a>
-  <a href="https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main" title="TODOs">
-    <img src="https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main">
-  </a>
-  <a href="https://app.bountysource.com/teams/gitea" title="Bountysource">
-    <img src="https://img.shields.io/bountysource/team/gitea/activity">
-  </a>
-</p>
+[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
+[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
+[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
+[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
+[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
+[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
+[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
+[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
+[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/go-gitea/gitea)
+[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin")
+[![](https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main)](https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main "TODOs")
 
-<p align="center">
-  <a href="README.md">View this document in English</a>
-</p>
+[View this document in English](./README.md)
 
 ## 目标
 
diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index 2aa60780c4..ea73182a83 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -24,11 +24,21 @@
     "path": "codeberg.org/gusted/mcaptcha/LICENSE",
     "licenseText": "Copyright © 2022 William Zijl\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n"
   },
+  {
+    "name": "connectrpc.com/connect",
+    "path": "connectrpc.com/connect/LICENSE",
+    "licenseText": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2021-2024 The Connect Authors\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
+  },
   {
     "name": "dario.cat/mergo",
     "path": "dario.cat/mergo/LICENSE",
     "licenseText": "Copyright (c) 2013 Dario Castañé. All rights reserved.\nCopyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
   },
+  {
+    "name": "filippo.io/edwards25519",
+    "path": "filippo.io/edwards25519/LICENSE",
+    "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+  },
   {
     "name": "git.sr.ht/~mariusor/go-xsd-duration",
     "path": "git.sr.ht/~mariusor/go-xsd-duration/LICENSE",
@@ -234,11 +244,6 @@
     "path": "github.com/bradfitz/gomemcache/memcache/LICENSE",
     "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
   },
-  {
-    "name": "github.com/bufbuild/connect-go",
-    "path": "github.com/bufbuild/connect-go/LICENSE",
-    "licenseText": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2021-2022 Buf Technologies, Inc.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
-  },
   {
     "name": "github.com/buildkite/terminal-to-html/v3",
     "path": "github.com/buildkite/terminal-to-html/v3/LICENSE",
@@ -299,11 +304,6 @@
     "path": "github.com/davecgh/go-spew/spew/LICENSE",
     "licenseText": "ISC License\n\nCopyright (c) 2012-2016 Dave Collins \u003cdave@davec.name\u003e\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
   },
-  {
-    "name": "github.com/denisenkom/go-mssqldb",
-    "path": "github.com/denisenkom/go-mssqldb/LICENSE.txt",
-    "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
-  },
   {
     "name": "github.com/dgryski/go-rendezvous",
     "path": "github.com/dgryski/go-rendezvous/LICENSE",
@@ -535,8 +535,8 @@
     "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n     http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
   },
   {
-    "name": "github.com/golang/protobuf",
-    "path": "github.com/golang/protobuf/LICENSE",
+    "name": "github.com/golang/protobuf/proto",
+    "path": "github.com/golang/protobuf/proto/LICENSE",
     "licenseText": "Copyright 2010 The Go Authors.  All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n    * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n"
   },
   {
@@ -754,6 +754,16 @@
     "path": "github.com/microcosm-cc/bluemonday/LICENSE.md",
     "licenseText": "SPDX short identifier: BSD-3-Clause\nhttps://opensource.org/licenses/BSD-3-Clause\n\nCopyright (c) 2014, David Kitchen \u003cdavid@buro9.com\u003e\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the organisation (Microcosm) nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
   },
+  {
+    "name": "github.com/microsoft/go-mssqldb",
+    "path": "github.com/microsoft/go-mssqldb/LICENSE.txt",
+    "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\nCopyright (c) Microsoft Corporation.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+  },
+  {
+    "name": "github.com/microsoft/go-mssqldb/internal/github.com/swisscom/mssql-always-encrypted/pkg",
+    "path": "github.com/microsoft/go-mssqldb/internal/github.com/swisscom/mssql-always-encrypted/pkg/LICENSE.txt",
+    "licenseText": "Copyright (c) 2021 Swisscom (Switzerland) Ltd\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\n"
+  },
   {
     "name": "github.com/miekg/dns",
     "path": "github.com/miekg/dns/LICENSE",
@@ -1066,7 +1076,7 @@
   },
   {
     "name": "go.uber.org/zap",
-    "path": "go.uber.org/zap/LICENSE.txt",
+    "path": "go.uber.org/zap/LICENSE",
     "licenseText": "Copyright (c) 2016-2017 Uber Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
   },
   {
diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go
index 0db505ff9c..ab769f6d0c 100644
--- a/cmd/admin_regenerate.go
+++ b/cmd/admin_regenerate.go
@@ -4,8 +4,8 @@
 package cmd
 
 import (
-	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/modules/graceful"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	repo_service "code.gitea.io/gitea/services/repository"
 
 	"github.com/urfave/cli/v2"
@@ -42,5 +42,5 @@ func runRegenerateKeys(_ *cli.Context) error {
 	if err := initDB(ctx); err != nil {
 		return err
 	}
-	return asymkey_model.RewriteAllPublicKeys(ctx)
+	return asymkey_service.RewriteAllPublicKeys(ctx)
 }
diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go
index fefe18d39c..a257ce21c8 100644
--- a/cmd/admin_user_create.go
+++ b/cmd/admin_user_create.go
@@ -10,8 +10,8 @@ import (
 	auth_model "code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
 	pwd "code.gitea.io/gitea/modules/auth/password"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/urfave/cli/v2"
 )
@@ -123,10 +123,10 @@ func runCreateUser(c *cli.Context) error {
 		changePassword = c.Bool("must-change-password")
 	}
 
-	restricted := util.OptionalBoolNone
+	restricted := optional.None[bool]()
 
 	if c.IsSet("restricted") {
-		restricted = util.OptionalBoolOf(c.Bool("restricted"))
+		restricted = optional.Some(c.Bool("restricted"))
 	}
 
 	// default user visibility in app.ini
@@ -142,7 +142,7 @@ func runCreateUser(c *cli.Context) error {
 	}
 
 	overwriteDefault := &user_model.CreateUserOverwriteOptions{
-		IsActive:     util.OptionalBoolTrue,
+		IsActive:     optional.Some(true),
 		IsRestricted: restricted,
 	}
 
diff --git a/cmd/dump.go b/cmd/dump.go
index 69ecdcec12..da0a51d9ce 100644
--- a/cmd/dump.go
+++ b/cmd/dump.go
@@ -6,14 +6,13 @@ package cmd
 
 import (
 	"fmt"
-	"io"
 	"os"
 	"path"
 	"path/filepath"
 	"strings"
-	"time"
 
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/dump"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
@@ -25,89 +24,17 @@ import (
 	"github.com/urfave/cli/v2"
 )
 
-func addReader(w archiver.Writer, r io.ReadCloser, info os.FileInfo, customName string, verbose bool) error {
-	if verbose {
-		log.Info("Adding file %s", customName)
-	}
-
-	return w.Write(archiver.File{
-		FileInfo: archiver.FileInfo{
-			FileInfo:   info,
-			CustomName: customName,
-		},
-		ReadCloser: r,
-	})
-}
-
-func addFile(w archiver.Writer, filePath, absPath string, verbose bool) error {
-	file, err := os.Open(absPath)
-	if err != nil {
-		return err
-	}
-	defer file.Close()
-	fileInfo, err := file.Stat()
-	if err != nil {
-		return err
-	}
-
-	return addReader(w, file, fileInfo, filePath, verbose)
-}
-
-func isSubdir(upper, lower string) (bool, error) {
-	if relPath, err := filepath.Rel(upper, lower); err != nil {
-		return false, err
-	} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
-		return true, nil
-	}
-	return false, nil
-}
-
-type outputType struct {
-	Enum     []string
-	Default  string
-	selected string
-}
-
-func (o outputType) Join() string {
-	return strings.Join(o.Enum, ", ")
-}
-
-func (o *outputType) Set(value string) error {
-	for _, enum := range o.Enum {
-		if enum == value {
-			o.selected = value
-			return nil
-		}
-	}
-
-	return fmt.Errorf("allowed values are %s", o.Join())
-}
-
-func (o outputType) String() string {
-	if o.selected == "" {
-		return o.Default
-	}
-	return o.selected
-}
-
-var outputTypeEnum = &outputType{
-	Enum:    []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"},
-	Default: "zip",
-}
-
 // CmdDump represents the available dump sub-command.
 var CmdDump = &cli.Command{
-	Name:  "dump",
-	Usage: "Dump Gitea files and database",
-	Description: `Dump compresses all related files and database into zip file.
-It can be used for backup and capture Gitea server image to send to maintainer`,
-	Action: runDump,
+	Name:        "dump",
+	Usage:       "Dump Gitea files and database",
+	Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`,
+	Action:      runDump,
 	Flags: []cli.Flag{
 		&cli.StringFlag{
 			Name:    "file",
 			Aliases: []string{"f"},
-			Value:   fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix()),
-			Usage:   "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.",
+			Usage:   `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`,
 		},
 		&cli.BoolFlag{
 			Name:    "verbose",
@@ -160,64 +87,52 @@ It can be used for backup and capture Gitea server image to send to maintainer`,
 			Name:  "skip-index",
 			Usage: "Skip bleve index data",
 		},
-		&cli.GenericFlag{
+		&cli.StringFlag{
 			Name:  "type",
-			Value: outputTypeEnum,
-			Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()),
+			Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")),
 		},
 	},
 }
 
 func fatal(format string, args ...any) {
-	fmt.Fprintf(os.Stderr, format+"\n", args...)
 	log.Fatal(format, args...)
 }
 
 func runDump(ctx *cli.Context) error {
-	var file *os.File
-	fileName := ctx.String("file")
-	outType := ctx.String("type")
-	if fileName == "-" {
-		file = os.Stdout
-		setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
-	} else {
-		for _, suffix := range outputTypeEnum.Enum {
-			if strings.HasSuffix(fileName, "."+suffix) {
-				fileName = strings.TrimSuffix(fileName, "."+suffix)
-				break
-			}
-		}
-		fileName += "." + outType
-	}
 	setting.MustInstalled()
 
-	// make sure we are logging to the console no matter what the configuration tells us do to
-	// FIXME: don't use CfgProvider directly
-	if _, err := setting.CfgProvider.Section("log").NewKey("MODE", "console"); err != nil {
-		fatal("Setting logging mode to console failed: %v", err)
-	}
-	if _, err := setting.CfgProvider.Section("log.console").NewKey("STDERR", "true"); err != nil {
-		fatal("Setting console logger to stderr failed: %v", err)
-	}
-
-	// Set loglevel to Warn if quiet-mode is requested
-	if ctx.Bool("quiet") {
-		if _, err := setting.CfgProvider.Section("log.console").NewKey("LEVEL", "Warn"); err != nil {
-			fatal("Setting console log-level failed: %v", err)
-		}
-	}
-
-	if !setting.InstallLock {
-		log.Error("Is '%s' really the right config path?\n", setting.CustomConf)
-		return fmt.Errorf("gitea is not initialized")
-	}
-	setting.LoadSettings() // cannot access session settings otherwise
-
+	quite := ctx.Bool("quiet")
 	verbose := ctx.Bool("verbose")
-	if verbose && ctx.Bool("quiet") {
-		return fmt.Errorf("--quiet and --verbose cannot both be set")
+	if verbose && quite {
+		fatal("Option --quiet and --verbose cannot both be set")
 	}
 
+	// outFileName is either "-" or a file name (will be made absolute)
+	outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type"))
+	if outType == "" {
+		fatal("Invalid output type")
+	}
+
+	outFile := os.Stdout
+	if outFileName != "-" {
+		var err error
+		if outFileName, err = filepath.Abs(outFileName); err != nil {
+			fatal("Unable to get absolute path of dump file: %v", err)
+		}
+		if exist, _ := util.IsExist(outFileName); exist {
+			fatal("Dump file %q exists", outFileName)
+		}
+		if outFile, err = os.Create(outFileName); err != nil {
+			fatal("Unable to create dump file %q: %v", outFileName, err)
+		}
+		defer outFile.Close()
+	}
+
+	setupConsoleLogger(util.Iif(quite, log.WARN, log.INFO), log.CanColorStderr, os.Stderr)
+
+	setting.DisableLoggerInit()
+	setting.LoadSettings() // cannot access session settings otherwise
+
 	stdCtx, cancel := installSignals()
 	defer cancel()
 
@@ -226,44 +141,32 @@ func runDump(ctx *cli.Context) error {
 		return err
 	}
 
-	if err := storage.Init(); err != nil {
+	if err = storage.Init(); err != nil {
 		return err
 	}
 
-	if file == nil {
-		file, err = os.Create(fileName)
-		if err != nil {
-			fatal("Unable to open %s: %v", fileName, err)
-		}
-	}
-	defer file.Close()
-
-	absFileName, err := filepath.Abs(fileName)
-	if err != nil {
-		return err
-	}
-
-	var iface any
-	if fileName == "-" {
-		iface, err = archiver.ByExtension(fmt.Sprintf(".%s", outType))
-	} else {
-		iface, err = archiver.ByExtension(fileName)
-	}
+	archiverGeneric, err := archiver.ByExtension("." + outType)
 	if err != nil {
 		fatal("Unable to get archiver for extension: %v", err)
 	}
 
-	w, _ := iface.(archiver.Writer)
-	if err := w.Create(file); err != nil {
+	archiverWriter := archiverGeneric.(archiver.Writer)
+	if err := archiverWriter.Create(outFile); err != nil {
 		fatal("Creating archiver.Writer failed: %v", err)
 	}
-	defer w.Close()
+	defer archiverWriter.Close()
+
+	dumper := &dump.Dumper{
+		Writer:  archiverWriter,
+		Verbose: verbose,
+	}
+	dumper.GlobalExcludeAbsPath(outFileName)
 
 	if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
 		log.Info("Skip dumping local repositories")
 	} else {
 		log.Info("Dumping local repositories... %s", setting.RepoRootPath)
-		if err := addRecursiveExclude(w, "repos", setting.RepoRootPath, []string{absFileName}, verbose); err != nil {
+		if err := dumper.AddRecursiveExclude("repos", setting.RepoRootPath, nil); err != nil {
 			fatal("Failed to include repositories: %v", err)
 		}
 
@@ -276,8 +179,7 @@ func runDump(ctx *cli.Context) error {
 			if err != nil {
 				return err
 			}
-
-			return addReader(w, object, info, path.Join("data", "lfs", objPath), verbose)
+			return dumper.AddReader(object, info, path.Join("data", "lfs", objPath))
 		}); err != nil {
 			fatal("Failed to dump LFS objects: %v", err)
 		}
@@ -310,15 +212,13 @@ func runDump(ctx *cli.Context) error {
 		fatal("Failed to dump database: %v", err)
 	}
 
-	if err := addFile(w, "gitea-db.sql", dbDump.Name(), verbose); err != nil {
+	if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil {
 		fatal("Failed to include gitea-db.sql: %v", err)
 	}
 
-	if len(setting.CustomConf) > 0 {
-		log.Info("Adding custom configuration file from %s", setting.CustomConf)
-		if err := addFile(w, "app.ini", setting.CustomConf, verbose); err != nil {
-			fatal("Failed to include specified app.ini: %v", err)
-		}
+	log.Info("Adding custom configuration file from %s", setting.CustomConf)
+	if err = dumper.AddFile("app.ini", setting.CustomConf); err != nil {
+		fatal("Failed to include specified app.ini: %v", err)
 	}
 
 	if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
@@ -326,8 +226,8 @@ func runDump(ctx *cli.Context) error {
 	} else {
 		customDir, err := os.Stat(setting.CustomPath)
 		if err == nil && customDir.IsDir() {
-			if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is {
-				if err := addRecursiveExclude(w, "custom", setting.CustomPath, []string{absFileName}, verbose); err != nil {
+			if is, _ := dump.IsSubdir(setting.AppDataPath, setting.CustomPath); !is {
+				if err := dumper.AddRecursiveExclude("custom", setting.CustomPath, nil); err != nil {
 					fatal("Failed to include custom: %v", err)
 				}
 			} else {
@@ -364,8 +264,7 @@ func runDump(ctx *cli.Context) error {
 		excludes = append(excludes, setting.Attachment.Storage.Path)
 		excludes = append(excludes, setting.Packages.Storage.Path)
 		excludes = append(excludes, setting.Log.RootPath)
-		excludes = append(excludes, absFileName)
-		if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil {
+		if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
 			fatal("Failed to include data directory: %v", err)
 		}
 	}
@@ -377,8 +276,7 @@ func runDump(ctx *cli.Context) error {
 		if err != nil {
 			return err
 		}
-
-		return addReader(w, object, info, path.Join("data", "attachments", objPath), verbose)
+		return dumper.AddReader(object, info, path.Join("data", "attachments", objPath))
 	}); err != nil {
 		fatal("Failed to dump attachments: %v", err)
 	}
@@ -392,8 +290,7 @@ func runDump(ctx *cli.Context) error {
 		if err != nil {
 			return err
 		}
-
-		return addReader(w, object, info, path.Join("data", "packages", objPath), verbose)
+		return dumper.AddReader(object, info, path.Join("data", "packages", objPath))
 	}); err != nil {
 		fatal("Failed to dump packages: %v", err)
 	}
@@ -409,80 +306,23 @@ func runDump(ctx *cli.Context) error {
 			log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err)
 		}
 		if isExist {
-			if err := addRecursiveExclude(w, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil {
+			if err := dumper.AddRecursiveExclude("log", setting.Log.RootPath, nil); err != nil {
 				fatal("Failed to include log: %v", err)
 			}
 		}
 	}
 
-	if fileName != "-" {
-		if err = w.Close(); err != nil {
-			_ = util.Remove(fileName)
-			fatal("Failed to save %s: %v", fileName, err)
+	if outFileName == "-" {
+		log.Info("Finish dumping to stdout")
+	} else {
+		if err = archiverWriter.Close(); err != nil {
+			_ = os.Remove(outFileName)
+			fatal("Failed to save %q: %v", outFileName, err)
 		}
-
-		if err := os.Chmod(fileName, 0o600); err != nil {
+		if err = os.Chmod(outFileName, 0o600); err != nil {
 			log.Info("Can't change file access permissions mask to 0600: %v", err)
 		}
-	}
-
-	if fileName != "-" {
-		log.Info("Finish dumping in file %s", fileName)
-	} else {
-		log.Info("Finish dumping to stdout")
-	}
-
-	return nil
-}
-
-// addRecursiveExclude zips absPath to specified insidePath inside writer excluding excludeAbsPath
-func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeAbsPath []string, verbose bool) error {
-	absPath, err := filepath.Abs(absPath)
-	if err != nil {
-		return err
-	}
-	dir, err := os.Open(absPath)
-	if err != nil {
-		return err
-	}
-	defer dir.Close()
-
-	files, err := dir.Readdir(0)
-	if err != nil {
-		return err
-	}
-	for _, file := range files {
-		currentAbsPath := filepath.Join(absPath, file.Name())
-		currentInsidePath := path.Join(insidePath, file.Name())
-		if file.IsDir() {
-			if !util.SliceContainsString(excludeAbsPath, currentAbsPath) {
-				if err := addFile(w, currentInsidePath, currentAbsPath, false); err != nil {
-					return err
-				}
-				if err = addRecursiveExclude(w, currentInsidePath, currentAbsPath, excludeAbsPath, verbose); err != nil {
-					return err
-				}
-			}
-		} else {
-			// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
-			shouldAdd := file.Mode().IsRegular()
-			if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
-				target, err := filepath.EvalSymlinks(currentAbsPath)
-				if err != nil {
-					return err
-				}
-				targetStat, err := os.Stat(target)
-				if err != nil {
-					return err
-				}
-				shouldAdd = targetStat.Mode().IsRegular()
-			}
-			if shouldAdd {
-				if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil {
-					return err
-				}
-			}
-		}
+		log.Info("Finish dumping in file %s", outFileName)
 	}
 	return nil
 }
diff --git a/cmd/generate.go b/cmd/generate.go
index 4ab10da22a..90b32ecaf0 100644
--- a/cmd/generate.go
+++ b/cmd/generate.go
@@ -70,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error {
 }
 
 func runGenerateLfsJwtSecret(c *cli.Context) error {
-	_, jwtSecretBase64, err := generate.NewJwtSecretBase64()
+	_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
 	if err != nil {
 		return err
 	}
diff --git a/cmd/hook.go b/cmd/hook.go
index 6a3358853d..c04591d79e 100644
--- a/cmd/hook.go
+++ b/cmd/hook.go
@@ -448,23 +448,26 @@ Gitea or set your environment appropriately.`, "")
 
 func hookPrintResults(results []private.HookPostReceiveBranchResult) {
 	for _, res := range results {
-		if !res.Message {
-			continue
-		}
-
-		fmt.Fprintln(os.Stderr, "")
-		if res.Create {
-			fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", res.Branch)
-			fmt.Fprintf(os.Stderr, "  %s\n", res.URL)
-		} else {
-			fmt.Fprint(os.Stderr, "Visit the existing pull request:\n")
-			fmt.Fprintf(os.Stderr, "  %s\n", res.URL)
-		}
-		fmt.Fprintln(os.Stderr, "")
-		os.Stderr.Sync()
+		hookPrintResult(res.Message, res.Create, res.Branch, res.URL)
 	}
 }
 
+func hookPrintResult(output, isCreate bool, branch, url string) {
+	if !output {
+		return
+	}
+	fmt.Fprintln(os.Stderr, "")
+	if isCreate {
+		fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", branch)
+		fmt.Fprintf(os.Stderr, "  %s\n", url)
+	} else {
+		fmt.Fprint(os.Stderr, "Visit the existing pull request:\n")
+		fmt.Fprintf(os.Stderr, "  %s\n", url)
+	}
+	fmt.Fprintln(os.Stderr, "")
+	os.Stderr.Sync()
+}
+
 func pushOptions() map[string]string {
 	opts := make(map[string]string)
 	if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil {
@@ -691,6 +694,12 @@ Gitea or set your environment appropriately.`, "")
 	}
 	err = writeFlushPktLine(ctx, os.Stdout)
 
+	if err == nil {
+		for _, res := range resp.Results {
+			hookPrintResult(res.ShouldShowMessage, res.IsCreatePR, res.HeadBranch, res.URL)
+		}
+	}
+
 	return err
 }
 
diff --git a/cmd/keys.go b/cmd/keys.go
index ceeec48486..7fdbe16119 100644
--- a/cmd/keys.go
+++ b/cmd/keys.go
@@ -71,7 +71,7 @@ func runKeys(c *cli.Context) error {
 	ctx, cancel := installSignals()
 	defer cancel()
 
-	setup(ctx, false)
+	setup(ctx, c.Bool("debug"))
 
 	authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content)
 	// do not use handleCliResponseExtra or cli.NewExitError, if it exists immediately, it breaks some tests like Test_CmdKeys
diff --git a/cmd/serv.go b/cmd/serv.go
index 726663660b..90190a19db 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -63,21 +63,10 @@ func setup(ctx context.Context, debug bool) {
 		setupConsoleLogger(log.FATAL, false, os.Stderr)
 	}
 	setting.MustInstalled()
-	if debug {
-		setting.RunMode = "dev"
-	}
-
-	// Check if setting.RepoRootPath exists. It could be the case that it doesn't exist, this can happen when
-	// `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection.
 	if _, err := os.Stat(setting.RepoRootPath); err != nil {
-		if os.IsNotExist(err) {
-			_ = fail(ctx, "Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath)
-		} else {
-			_ = fail(ctx, "Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err)
-		}
+		_ = fail(ctx, "Unable to access repository path", "Unable to access repository path %q, err: %v", setting.RepoRootPath, err)
 		return
 	}
-
 	if err := git.InitSimple(context.Background()); err != nil {
 		_ = fail(ctx, "Failed to init git", "Failed to init git, err: %v", err)
 	}
@@ -216,16 +205,18 @@ func runServ(c *cli.Context) error {
 		}
 	}
 
-	// LowerCase and trim the repoPath as that's how they are stored.
-	repoPath = strings.ToLower(strings.TrimSpace(repoPath))
-
 	rr := strings.SplitN(repoPath, "/", 2)
 	if len(rr) != 2 {
 		return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath)
 	}
 
-	username := strings.ToLower(rr[0])
-	reponame := strings.ToLower(strings.TrimSuffix(rr[1], ".git"))
+	username := rr[0]
+	reponame := strings.TrimSuffix(rr[1], ".git")
+
+	// LowerCase and trim the repoPath as that's how they are stored.
+	// This should be done after splitting the repoPath into username and reponame
+	// so that username and reponame are not affected.
+	repoPath = strings.ToLower(strings.TrimSpace(repoPath))
 
 	if alphaDashDotPattern.MatchString(reponame) {
 		return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
diff --git a/cmd/web.go b/cmd/web.go
index 01386251be..ef8a7426c1 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -114,7 +114,7 @@ func showWebStartupMessage(msg string) {
 	log.Info("* WorkPath: %s", setting.AppWorkPath)
 	log.Info("* CustomPath: %s", setting.CustomPath)
 	log.Info("* ConfigFile: %s", setting.CustomConf)
-	log.Info("%s", msg)
+	log.Info("%s", msg) // show startup message
 }
 
 func serveInstall(ctx *cli.Context) error {
diff --git a/contrib/systemd/gitea.service b/contrib/systemd/gitea.service
index d205c6ee8b..c091722a74 100644
--- a/contrib/systemd/gitea.service
+++ b/contrib/systemd/gitea.service
@@ -1,6 +1,5 @@
 [Unit]
 Description=Gitea (Git with a cup of tea)
-After=syslog.target
 After=network.target
 ###
 # Don't forget to add the database service dependencies
diff --git a/crowdin.yml b/crowdin.yml
new file mode 100644
index 0000000000..35a38d768c
--- /dev/null
+++ b/crowdin.yml
@@ -0,0 +1,12 @@
+project_id_env: CROWDIN_PROJECT_ID
+api_token_env: CROWDIN_KEY
+base_path: "."
+base_url: "https://api.crowdin.com"
+preserve_hierarchy: true
+files:
+  - source: "/options/locale/locale_en-US.ini"
+    translation: "/options/locale/locale_%locale%.ini"
+    type: "ini"
+    skip_untranslated_strings: true
+    export_only_approved: true
+    update_option: "update_as_unapproved"
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 363bbcb151..918252044b 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -412,6 +412,10 @@ USER = root
 ;;
 ;; Whether execute database models migrations automatically
 ;AUTO_MIGRATION = true
+;;
+;; Threshold value (in seconds) beyond which query execution time is logged as a warning in the xorm logger
+;;
+;SLOW_QUERY_THRESHOLD = 5s
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -437,7 +441,7 @@ INTERNAL_TOKEN =
 ;INTERNAL_TOKEN_URI = file:/etc/gitea/internal_token
 ;;
 ;; How long to remember that a user is logged in before requiring relogin (in days)
-;LOGIN_REMEMBER_DAYS = 7
+;LOGIN_REMEMBER_DAYS = 31
 ;;
 ;; Name of the cookie used to store the current username.
 ;COOKIE_USERNAME = gitea_awesome
@@ -952,6 +956,12 @@ LEVEL = Info
 ;GO_GET_CLONE_URL_PROTOCOL = https
 ;;
 ;; Close issues as long as a commit on any branch marks it as fixed
+;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
+;;
+;; Allow users to push local repositories to Gitea and have them automatically created for a user or an org
+;ENABLE_PUSH_CREATE_USER = false
+;ENABLE_PUSH_CREATE_ORG = false
+;;
 ;; Comma separated list of globally disabled repo units. Allowed values: repo.issues, repo.ext_issues, repo.pulls, repo.wiki, repo.ext_wiki, repo.projects, repo.packages, repo.actions.
 ;DISABLED_REPO_UNITS =
 ;;
@@ -1044,7 +1054,7 @@ LEVEL = Info
 ;; List of keywords used in Pull Request comments to automatically reopen a related issue
 ;REOPEN_KEYWORDS = reopen,reopens,reopened
 ;;
-;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash
+;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash, fast-forward-only
 ;DEFAULT_MERGE_STYLE = merge
 ;;
 ;; In the default merge message for squash commits include at most this many commits
@@ -1470,6 +1480,16 @@ LEVEL = Info
 ;;
 ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
 ;DEFAULT_EMAIL_NOTIFICATIONS = enabled
+;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future
+;; - deletion: a user cannot delete their own account
+;; - manage_ssh_keys: a user cannot configure ssh keys
+;; - manage_gpg_keys: a user cannot configure gpg keys
+;USER_DISABLED_FEATURES =
+;; Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior.
+;; - deletion: a user cannot delete their own account
+;; - manage_ssh_keys: a user cannot configure ssh keys
+;; - manage_gpg_keys: a user cannot configure gpg keys
+;;EXTERNAL_USER_DISABLE_FEATURES =
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2295,6 +2315,8 @@ LEVEL = Info
 ;SHOW_FOOTER_VERSION = true
 ;; Show template execution time in the footer
 ;SHOW_FOOTER_TEMPLATE_LOAD_TIME = true
+;; Show the "powered by" text in the footer
+;SHOW_FOOTER_POWERED_BY = true
 ;; Generate sitemap. Defaults to `true`.
 ;ENABLE_SITEMAP = true
 ;; Enable/Disable RSS/Atom feed
@@ -2593,7 +2615,7 @@ LEVEL = Info
 ;ENDLESS_TASK_TIMEOUT = 3h
 ;; Timeout to cancel the jobs which have waiting status, but haven't been picked by a runner for a long time
 ;ABANDONED_JOB_TIMEOUT = 24h
-;; Strings committers can place inside a commit message to skip executing the corresponding actions workflow
+;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow
 ;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip]
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/docs/content/administration/backup-and-restore.en-us.md b/docs/content/administration/backup-and-restore.en-us.md
index d46efecf99..451ef5c944 100644
--- a/docs/content/administration/backup-and-restore.en-us.md
+++ b/docs/content/administration/backup-and-restore.en-us.md
@@ -92,7 +92,7 @@ cd gitea-dump-1610949662
 mv app.ini /etc/gitea/conf/app.ini
 mv data/* /var/lib/gitea/data/
 mv log/* /var/lib/gitea/log/
-mv repos/* /var/lib/gitea/gitea-repositories/
+mv repos/* /var/lib/gitea/data/gitea-repositories/
 chown -R gitea:gitea /etc/gitea/conf/app.ini /var/lib/gitea
 
 # mysql
@@ -111,6 +111,8 @@ With Gitea running, and from the directory Gitea's binary is located, execute: `
 
 This ensures that application and configuration file paths in repository Git Hooks are consistent and applicable to the current installation. If these paths are not updated, repository `push` actions will fail.
 
+If you still have issues, consider running `./gitea doctor check` to inspect possible errors (or run with `--fix`).
+
 ### Using Docker (`restore`)
 
 There is also no support for a recovery command in a Docker-based gitea instance. The restore process contains the same steps as described in the previous section but with different paths.
diff --git a/docs/content/administration/backup-and-restore.zh-cn.md b/docs/content/administration/backup-and-restore.zh-cn.md
index 98d378d5dc..db7eba84f7 100644
--- a/docs/content/administration/backup-and-restore.zh-cn.md
+++ b/docs/content/administration/backup-and-restore.zh-cn.md
@@ -19,6 +19,12 @@ menu:
 
 Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一个zip压缩文件。该压缩文件可以被用来进行数据恢复。
 
+## 备份一致性
+
+为了确保 Gitea 实例的一致性,在备份期间必须关闭它。
+
+Gitea 包括数据库、文件和 Git 仓库,当它被使用时所有这些都会发生变化。例如,当迁移正在进行时,在数据库中创建一个事务,而 Git 仓库正在被复制。如果备份发生在迁移的中间,Git 仓库可能是不完整的,尽管数据库声称它是完整的,因为它是在之后被转储的。避免这种竞争条件的唯一方法是在备份期间停止 Gitea 实例。
+
 ## 备份命令 (`dump`)
 
 先转到git用户的权限: `su git`. 再Gitea目录运行 `./gitea dump`。一般会显示类似如下的输出:
@@ -34,15 +40,43 @@ Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一
 
 最后生成的 `gitea-dump-1482906742.zip` 文件将会包含如下内容:
 
-* `custom` - 所有保存在 `custom/` 目录下的配置和自定义的文件。
-* `data` - 数据目录下的所有内容不包含使用文件session的文件。该目录包含 `attachments`, `avatars`, `lfs`, `indexers`, 如果使用sqlite 还会包含 sqlite 数据库文件。
+* `app.ini` - 如果原先存储在默认的 custom/ 目录之外,则是配置文件的可选副本
+* `custom/` - 所有保存在 `custom/` 目录下的配置和自定义的文件。
+* `data/` - 数据目录(APP_DATA_PATH),如果使用文件会话,则不包括会话。该目录包括 `attachments`、`avatars`、`lfs`、`indexers`、如果使用 SQLite 则包括 SQLite 文件。
+* `repos/` - 仓库目录的完整副本。
 * `gitea-db.sql` - 数据库dump出来的 SQL。
-* `gitea-repo.zip` - Git仓库压缩文件。
 * `log/` - Logs文件,如果用作迁移不是必须的。
 
 中间备份文件将会在临时目录进行创建,如果您要重新指定临时目录,可以用 `--tempdir` 参数,或者用 `TMPDIR` 环境变量。
 
-## Restore Command (`restore`)
+## 备份数据库
+
+`gitea dump` 创建的 SQL 转储使用 XORM,Gitea 管理员可能更喜欢使用本地的 MySQL 和 PostgreSQL 转储工具。使用 XORM 转储数据库时仍然存在一些问题,可能会导致在尝试恢复时出现问题。
+
+```sh
+# mysql
+mysqldump -u$USER -p$PASS --database $DATABASE > gitea-db.sql
+# postgres
+pg_dump -U $USER $DATABASE > gitea-db.sql
+```
+
+### 使用Docker (`dump`)
+
+在使用 Docker 时,使用 `dump` 命令有一些注意事项。
+
+必须以 `gitea/conf/app.ini` 中指定的 `RUN_USER = <OS_USERNAME>` 执行该命令;并且,为了让备份文件夹的压缩过程能够顺利执行,`docker exec` 命令必须在 `--tempdir` 内部执行。
+
+示例:
+
+```none
+docker exec -u <OS_USERNAME> -it -w <--tempdir> $(docker ps -qf 'name=^<NAME_OF_DOCKER_CONTAINER>$') bash -c '/usr/local/bin/gitea dump -c </path/to/app.ini>'
+```
+
+\*注意:`--tempdir` 指的是 Gitea 使用的 Docker 环境的临时目录;如果您没有指定自定义的 `--tempdir`,那么 Gitea 将使用 `/tmp` 或 Docker 容器的 `TMPDIR` 环境变量。对于 `--tempdir`,请相应调整您的 `docker exec` 命令选项。
+
+结果应该是一个文件,存储在指定的 `--tempdir` 中,类似于:`gitea-dump-1482906742.zip`
+
+## 恢复命令 (`restore`)
 
 当前还没有恢复命令,恢复需要人工进行。主要是把文件和数据库进行恢复。
 
@@ -51,10 +85,10 @@ Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一
 ```sh
 unzip gitea-dump-1610949662.zip
 cd gitea-dump-1610949662
-mv data/conf/app.ini /etc/gitea/conf/app.ini
+mv app.ini /etc/gitea/conf/app.ini
 mv data/* /var/lib/gitea/data/
 mv log/* /var/lib/gitea/log/
-mv repos/* /var/lib/gitea/repositories/
+mv repos/* /var/lib/gitea/gitea-repositories/
 chown -R gitea:gitea /etc/gitea/conf/app.ini /var/lib/gitea
 
 # mysql
@@ -66,3 +100,55 @@ psql -U $USER -d $DATABASE < gitea-db.sql
 
 service gitea restart
 ```
+
+如果安装方式发生了变化(例如 二进制 -> Docker),或者 Gitea 安装到了与之前安装不同的目录,则需要重新生成仓库 Git 钩子。
+
+在 Gitea 运行时,并从 Gitea 二进制文件所在的目录执行:`./gitea admin regenerate hooks`
+
+这样可以确保仓库 Git 钩子中的应用程序和配置文件路径与当前安装一致。如果这些路径没有更新,仓库的 `push` 操作将失败。
+
+### 使用 Docker (`restore`)
+
+在基于 Docker 的 Gitea 实例中,也没有恢复命令的支持。恢复过程与前面描述的步骤相同,但路径不同。
+
+示例:
+
+```sh
+# 在容器中打开 bash 会话
+docker exec --user git -it 2a83b293548e bash
+# 在容器内解压您的备份文件
+unzip gitea-dump-1610949662.zip
+cd gitea-dump-1610949662
+# 恢复 Gitea 数据
+mv data/* /data/gitea
+# 恢复仓库本身
+mv repos/* /data/git/gitea-repositories/
+# 调整文件权限
+chown -R git:git /data
+# 重新生成 Git 钩子
+/usr/local/bin/gitea -c '/data/gitea/conf/app.ini' admin regenerate hooks
+```
+
+Gitea 容器中的默认用户是 `git`(1000:1000)。请用您的 Gitea 容器 ID 或名称替换 `2a83b293548e`。
+
+### 使用 Docker-rootless (`restore`)
+
+在 Docker-rootless 容器中的恢复工作流程只是要使用的目录不同:
+
+```sh
+# 在容器中打开 bash 会话
+docker exec --user git -it 2a83b293548e bash
+# 在容器内解压您的备份文件
+unzip gitea-dump-1610949662.zip
+cd gitea-dump-1610949662
+# 恢复 app.ini
+mv data/conf/app.ini /etc/gitea/app.ini
+# 恢复 Gitea 数据
+mv data/* /var/lib/gitea
+# 恢复仓库本身
+mv repos/* /var/lib/gitea/git/gitea-repositories
+# 调整文件权限
+chown -R git:git /etc/gitea/app.ini /var/lib/gitea
+# 重新生成 Git 钩子
+/usr/local/bin/gitea -c '/etc/gitea/app.ini' admin regenerate hooks
+```
diff --git a/docs/content/administration/cmd-embedded.zh-cn.md b/docs/content/administration/cmd-embedded.zh-cn.md
index 4570bb58a3..a2df1aa2f5 100644
--- a/docs/content/administration/cmd-embedded.zh-cn.md
+++ b/docs/content/administration/cmd-embedded.zh-cn.md
@@ -37,7 +37,7 @@ gitea embedded list [--include-vendored] [patterns...]
 
 - 列出所有模板文件,无论在哪个虚拟目录下:`**.tmpl`
 - 列出所有邮件模板文件:`templates/mail/**.tmpl`
-- 列出 `public/img` 目录下的所有文件:`public/img/**`
+列出 `public/assets/img` 目录下的所有文件:`public/assets/img/**`
 
 不要忘记为模式使用引号,因为空格、`*` 和其他字符可能对命令行解释器有特殊含义。
 
@@ -49,8 +49,8 @@ gitea embedded list [--include-vendored] [patterns...]
 
 ```sh
 $ gitea embedded list '**openid**'
-public/img/auth/openid_connect.svg
-public/img/openid-16x16.png
+public/assets/img/auth/openid_connect.svg
+public/assets/img/openid-16x16.png
 templates/user/auth/finalize_openid.tmpl
 templates/user/auth/signin_openid.tmpl
 templates/user/auth/signup_openid_connect.tmpl
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 33732d080b..9de7511964 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -126,7 +126,7 @@ In addition, there is _`StaticRootPath`_ which can be set as a built-in at build
  keywords used in Pull Request comments to automatically close a related issue
 - `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: List of keywords used in Pull Request comments to automatically reopen
  a related issue
-- `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash`
+- `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash`, `fast-forward-only`
 - `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: In the default merge message for squash commits include at most this many commits. Set to `-1` to include all commits
 - `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: In the default merge message for squash commits limit the size of the commit messages. Set to `-1` to have no limit. Only used if `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES` is `true`.
 - `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: In the default merge message for squash commits walk all commits to include all authors in the Co-authored-by otherwise just use those in the limited list
@@ -458,6 +458,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
 - `MAX_IDLE_CONNS` **2**: Max idle database connections on connection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`.
 - `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071).
 - `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically.
+- `SLOW_QUERY_THRESHOLD` **5s**: Threshold value in seconds beyond which query execution time is logged as a warning in the xorm logger.
 
 [^1]: It may be necessary to specify a hostport even when listening on a unix socket, as the port is part of the socket name. see [#24552](https://github.com/go-gitea/gitea/issues/24552#issuecomment-1681649367) for additional details.
 
@@ -517,13 +518,21 @@ And the following unique queues:
 
 - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
 - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations.
+- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys` and more features can be added in future.
+  - `deletion`: User cannot delete their own account.
+  - `manage_ssh_keys`: User cannot configure ssh keys.
+  - `manage_gpg_keys`: User cannot configure gpg keys.
+- `EXTERNAL_USER_DISABLE_FEATURES`: **_empty_**: Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior.
+  - `deletion`: User cannot delete their own account.
+  - `manage_ssh_keys`: User cannot configure ssh keys.
+  - `manage_gpg_keys`: User cannot configure gpg keys.
 
 ## Security (`security`)
 
 - `INSTALL_LOCK`: **false**: Controls access to the installation page. When set to "true", the installation page is not accessible.
 - `SECRET_KEY`: **\<random at every install\>**: Global secret key. This key is VERY IMPORTANT, if you lost it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
 - `SECRET_KEY_URI`: **_empty_**: Instead of defining SECRET_KEY, this option can be used to use the key stored in a file (example value: `file:/etc/gitea/secret_key`). It shouldn't be lost like SECRET_KEY.
-- `LOGIN_REMEMBER_DAYS`: **7**: Cookie lifetime, in days.
+- `LOGIN_REMEMBER_DAYS`: **31**: How long to remember that a user is logged in before requiring relogin (in days).
 - `COOKIE_REMEMBER_NAME`: **gitea\_incredible**: Name of cookie used to store authentication
    information.
 - `REVERSE_PROXY_AUTHENTICATION_USER`: **X-WEBAUTH-USER**: Header name for reverse proxy
@@ -585,7 +594,7 @@ And the following unique queues:
 
 ## OpenID (`openid`)
 
-- `ENABLE_OPENID_SIGNIN`: **false**: Allow authentication in via OpenID.
+- `ENABLE_OPENID_SIGNIN`: **true**: Allow authentication in via OpenID.
 - `ENABLE_OPENID_SIGNUP`: **! DISABLE\_REGISTRATION**: Allow registering via OpenID.
 - `WHITELISTED_URIS`: **_empty_**: If non-empty, list of POSIX regex patterns matching
    OpenID URI's to permit.
@@ -827,7 +836,7 @@ Default templates for project boards:
 ## Issue and pull request attachments (`attachment`)
 
 - `ENABLED`: **true**: Whether issue and pull request attachments are enabled.
-- `ALLOWED_TYPES`: **.csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
+- `ALLOWED_TYPES`: **.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
 - `MAX_SIZE`: **2048**: Maximum size (MB).
 - `MAX_FILES`: **5**: Maximum number of attachments that can be uploaded at once.
 - `STORAGE_TYPE`: **local**: Storage type for attachments, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]`
@@ -1401,7 +1410,7 @@ PROXY_HOSTS = *.github.com
 - `ZOMBIE_TASK_TIMEOUT`: **10m**: Timeout to stop the task which have running status, but haven't been updated for a long time
 - `ENDLESS_TASK_TIMEOUT`: **3h**: Timeout to stop the tasks which have running status and continuous updates, but don't end for a long time
 - `ABANDONED_JOB_TIMEOUT`: **24h**: Timeout to cancel the jobs which have waiting status, but haven't been picked by a runner for a long time
-- `SKIP_WORKFLOW_STRINGS`: **[skip ci],[ci skip],[no ci],[skip actions],[actions skip]**: Strings committers can place inside a commit message to skip executing the corresponding actions workflow
+- `SKIP_WORKFLOW_STRINGS`: **[skip ci],[ci skip],[no ci],[skip actions],[actions skip]**: Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow
 
 `DEFAULT_ACTIONS_URL` indicates where the Gitea Actions runners should find the actions with relative path.
 For example, `uses: actions/checkout@v4` means `https://github.com/actions/checkout@v4` since the value of `DEFAULT_ACTIONS_URL` is `github`.
@@ -1420,5 +1429,6 @@ Like `uses: https://gitea.com/actions/checkout@v4` or `uses: http://your-git-ser
 
 - `SHOW_FOOTER_VERSION`: **true**: Show Gitea and Go version information in the footer.
 - `SHOW_FOOTER_TEMPLATE_LOAD_TIME`: **true**: Show time of template execution in the footer.
+- `SHOW_FOOTER_POWERED_BY`: **true**: Show the "powered by" text in the footer.
 - `ENABLE_SITEMAP`: **true**: Generate sitemap.
 - `ENABLE_FEED`: **true**: Enable/Disable RSS/Atom feed.
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 2cee70daab..759f39b576 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -29,7 +29,7 @@ menu:
 [ini](https://github.com/go-ini/ini/#recursive-values) 这里的说明。
 标注了 :exclamation: 的配置项表明除非你真的理解这个配置项的意义,否则最好使用默认值。
 
-在下面的默认值中,`$XYZ`代表环境变量`XYZ`的值(详见:`enviroment-to-ini`)。 _`XxYyZz`_是指默认配置的一部分列出的值。这些在 app.ini 文件中不起作用,仅在此处列出作为文档说明。
+在下面的默认值中,`$XYZ`代表环境变量`XYZ`的值(详见:`environment-to-ini`)。 _`XxYyZz`_是指默认配置的一部分列出的值。这些在 app.ini 文件中不起作用,仅在此处列出作为文档说明。
 
 包含`#`或者`;`的变量必须使用引号(`` ` ``或者`""""`)包裹,否则会被解析为注释。
 
@@ -125,7 +125,7 @@ menu:
 - `CLOSE_KEYWORDS`: **close**, **closes**, **closed**, **fix**, **fixes**, **fixed**, **resolve**, **resolves**, **resolved**: 在拉取请求评论中用于自动关闭相关问题的关键词列表。
 - `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: 在拉取请求评论中用于自动重新打开相关问题的
 关键词列表。
-- `DEFAULT_MERGE_STYLE`: **merge**: 设置创建仓库的默认合并方式,可选: `merge`, `rebase`, `rebase-merge`, `squash`
+- `DEFAULT_MERGE_STYLE`: **merge**: 设置创建仓库的默认合并方式,可选: `merge`, `rebase`, `rebase-merge`, `squash`, `fast-forward-only`
 - `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: 在默认合并消息中,对于`squash`提交,最多包括此数量的提交。设置为 -1 以包括所有提交。
 - `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: 在默认的合并消息中,对于`squash`提交,限制提交消息的大小。设置为 `-1`以取消限制。仅在`POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`为`true`时使用。
 - `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: 在默认合并消息中,对于`squash`提交,遍历所有提交以包括所有作者的`Co-authored-by`,否则仅使用限定列表中的作者。
@@ -497,13 +497,17 @@ Gitea 创建以下非唯一队列:
 
 - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**:用户电子邮件通知的默认配置(用户可配置)。选项:enabled、onmention、disabled
 - `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。
+- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_ssh_keys`, `manage_gpg_keys` 未来可以增加更多设置。
+  - `deletion`: 用户不能通过界面或者API删除他自己。
+  - `manage_ssh_keys`: 用户不能通过界面或者API配置SSH Keys。
+  - `manage_gpg_keys`: 用户不能配置 GPG 密钥。
 
 ## 安全性 (`security`)
 
 - `INSTALL_LOCK`: **false**:控制是否能够访问安装向导页面,设置为 `true` 则禁止访问安装向导页面。
 - `SECRET_KEY`: **\<每次安装时随机生成\>**:全局服务器安全密钥。这个密钥非常重要,如果丢失将无法解密加密的数据(例如 2FA)。
 - `SECRET_KEY_URI`: **_empty_**:与定义 `SECRET_KEY` 不同,此选项可用于使用存储在文件中的密钥(示例值:`file:/etc/gitea/secret_key`)。它不应该像 `SECRET_KEY` 一样容易丢失。
-- `LOGIN_REMEMBER_DAYS`: **7**:Cookie 保存时间,单位为天。
+- `LOGIN_REMEMBER_DAYS`: **31**:在要求重新登录之前,记住用户的登录状态多长时间(以天为单位)。
 - `COOKIE_REMEMBER_NAME`: **gitea\_incredible**:保存自动登录信息的 Cookie 名称。
 - `REVERSE_PROXY_AUTHENTICATION_USER`: **X-WEBAUTH-USER**:反向代理认证的 HTTP 头部名称,用于提供用户信息。
 - `REVERSE_PROXY_AUTHENTICATION_EMAIL`: **X-WEBAUTH-EMAIL**:反向代理认证的 HTTP 头部名称,用于提供邮箱信息。
@@ -558,7 +562,7 @@ Gitea 创建以下非唯一队列:
 
 ## OpenID (`openid`)
 
-- `ENABLE_OPENID_SIGNIN`: **false**:允许通过OpenID进行身份验证。
+- `ENABLE_OPENID_SIGNIN`: **true**:允许通过OpenID进行身份验证。
 - `ENABLE_OPENID_SIGNUP`: **! DISABLE\_REGISTRATION**:允许通过OpenID进行注册。
 - `WHITELISTED_URIS`: **_empty_**:如果非空,是一组匹配OpenID URI的POSIX正则表达式模式,用于允许访问。
 - `BLACKLISTED_URIS`: **_empty_**:如果非空,是一组匹配OpenID URI的POSIX正则表达式模式,用于阻止访问。
@@ -778,7 +782,7 @@ Gitea 创建以下非唯一队列:
 ## 工单和合并请求的附件 (`attachment`)
 
 - `ENABLED`: **true**: 是否允许用户上传附件。
-- `ALLOWED_TYPES`: **.csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: 允许的文件扩展名(`.zip`)、mime 类型(`text/plain`)或通配符类型(`image/*`、`audio/*`、`video/*`)的逗号分隔列表。空值或 `*/*` 允许所有类型。
+- `ALLOWED_TYPES`: **.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: 允许的文件扩展名(`.zip`)、mime 类型(`text/plain`)或通配符类型(`image/*`、`audio/*`、`video/*`)的逗号分隔列表。空值或 `*/*` 允许所有类型。
 - `MAX_SIZE`: **2048**: 附件的最大限制(MB)。
 - `MAX_FILES`: **5**: 一次最多上传的附件数量。
 - `STORAGE_TYPE`: **local**: 附件的存储类型,`local` 表示本地磁盘,`minio` 表示兼容 S3 的对象存储服务,如果未设置将使用默认值 `local` 或其他在 `[storage.xxx]` 中定义的名称。
@@ -1349,5 +1353,6 @@ PROXY_HOSTS = *.github.com
 
 - `SHOW_FOOTER_VERSION`: **true**: 在页面底部显示Gitea的版本。
 - `SHOW_FOOTER_TEMPLATE_LOAD_TIME`: **true**: 在页脚显示模板执行的时间。
+- `SHOW_FOOTER_POWERED_BY`: **true**: 在页脚显示“由...提供动力”的文本。
 - `ENABLE_SITEMAP`: **true**: 生成sitemap.
 - `ENABLE_FEED`: **true**: 是否启用RSS/Atom
diff --git a/docs/content/administration/customizing-gitea.en-us.md b/docs/content/administration/customizing-gitea.en-us.md
index d122fb4bfa..7efddb2824 100644
--- a/docs/content/administration/customizing-gitea.en-us.md
+++ b/docs/content/administration/customizing-gitea.en-us.md
@@ -284,7 +284,7 @@ syntax and shouldn't be touched without fully understanding these components.
 
 Google Analytics, Matomo (previously Piwik), and other analytics services can be added to Gitea. To add the tracking code, refer to the `Other additions to the page` section of this document, and add the JavaScript to the `$GITEA_CUSTOM/templates/custom/header.tmpl` file.
 
-## Customizing gitignores, labels, licenses, locales, and readmes.
+## Customizing gitignores, labels, licenses, locales, and readmes
 
 Place custom files in corresponding sub-folder under `custom/options`.
 
diff --git a/docs/content/administration/https-support.en-us.md b/docs/content/administration/https-support.en-us.md
index 4e18722ddf..981a29bd85 100644
--- a/docs/content/administration/https-support.en-us.md
+++ b/docs/content/administration/https-support.en-us.md
@@ -35,7 +35,7 @@ CERT_FILE = cert.pem
 KEY_FILE  = key.pem
 ```
 
-Note that if your certificate is signed by a third party certificate authority (i.e. not self-signed), then cert.pem should contain the certificate chain. The server certificate must be the first entry in cert.pem, followed by the intermediaries in order (if any). The root certificate does not have to be included because the connecting client must already have it in order to estalbish the trust relationship.
+Note that if your certificate is signed by a third party certificate authority (i.e. not self-signed), then cert.pem should contain the certificate chain. The server certificate must be the first entry in cert.pem, followed by the intermediaries in order (if any). The root certificate does not have to be included because the connecting client must already have it in order to establish the trust relationship.
 To learn more about the config values, please checkout the [Config Cheat Sheet](administration/config-cheat-sheet.md#server-server).
 
 For the `CERT_FILE` or `KEY_FILE` field, the file path is relative to the `GITEA_CUSTOM` environment variable when it is a relative path. It can be an absolute path as well.
diff --git a/docs/content/administration/mail-templates.en-us.md b/docs/content/administration/mail-templates.en-us.md
index 32b352da4b..8e4e416e8d 100644
--- a/docs/content/administration/mail-templates.en-us.md
+++ b/docs/content/administration/mail-templates.en-us.md
@@ -163,7 +163,7 @@ clients don't even support HTML, so they show the text version included in the g
 
 If the template fails to render, it will be noticed only at the moment the mail is sent.
 A default subject is used if the subject template fails, and whatever was rendered successfully
-from the the _mail body_ is used, disregarding the rest.
+from the _mail body_ is used, disregarding the rest.
 
 Please check [Gitea's logs](administration/logging-config.md) for error messages in case of trouble.
 
@@ -222,9 +222,9 @@ Please check [Gitea's logs](administration/logging-config.md) for error messages
         <a href="{{.Link}}">{{.Repo}}#{{.Issue.Index}}</a>.
         </p>
         {{if not (eq .Body "")}}
-            <h3>Message content:</h3>
+            <h3>Message content</h3>
             <hr>
-            {{.Body | Str2html}}
+            {{.Body}}
         {{end}}
     </p>
     <hr>
@@ -245,7 +245,7 @@ This template produces something along these lines:
 
 > [@rhonda](#) (Rhonda Myers) updated [mike/stuff#38](#).
 >
-> #### Message content:
+> #### Message content
 >
 > \_********************************\_********************************
 >
@@ -259,20 +259,20 @@ This template produces something along these lines:
 The template system contains several functions that can be used to further process and format
 the messages. Here's a list of some of them:
 
-| Name             | Parameters  | Available | Usage                                                                       |
-| ---------------- | ----------- | --------- | --------------------------------------------------------------------------- |
-| `AppUrl`         | -           | Any       | Gitea's URL                                                                 |
-| `AppName`        | -           | Any       | Set from `app.ini`, usually "Gitea"                                         |
-| `AppDomain`      | -           | Any       | Gitea's host name                                                           |
-| `EllipsisString` | string, int | Any       | Truncates a string to the specified length; adds ellipsis as needed         |
-| `Str2html`       | string      | Body only | Sanitizes text by removing any HTML tags from it.                           |
-| `Safe`           | string      | Body only | Takes the input as HTML; can be used for `.ReviewComments.RenderedContent`. |
+| Name             | Parameters  | Available | Usage                                                               |
+| ---------------- | ----------- | --------- | ------------------------------------------------------------------- |
+| `AppUrl`         | -           | Any       | Gitea's URL                                                         |
+| `AppName`        | -           | Any       | Set from `app.ini`, usually "Gitea"                                 |
+| `AppDomain`      | -           | Any       | Gitea's host name                                                   |
+| `EllipsisString` | string, int | Any       | Truncates a string to the specified length; adds ellipsis as needed |
+| `SanitizeHTML`   | string      | Body only | Sanitizes text by removing any dangerous HTML tags from it          |
+| `SafeHTML`       | string      | Body only | Takes the input as HTML, can be used for outputing raw HTML content |
 
 These are _functions_, not metadata, so they have to be used:
 
 ```html
-Like this:         {{Str2html "Escape<my>text"}}
-Or this:           {{"Escape<my>text" | Str2html}}
+Like this:         {{SanitizeHTML "Escape<my>text"}}
+Or this:           {{"Escape<my>text" | SanitizeHTML}}
 Or this:           {{AppUrl}}
 But not like this: {{.AppUrl}}
 ```
diff --git a/docs/content/administration/mail-templates.zh-cn.md b/docs/content/administration/mail-templates.zh-cn.md
index 588f0b2ccb..3c7c2a9397 100644
--- a/docs/content/administration/mail-templates.zh-cn.md
+++ b/docs/content/administration/mail-templates.zh-cn.md
@@ -207,7 +207,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
         {{if not (eq .Body "")}}
             <h3>消息内容:</h3>
             <hr>
-            {{.Body | Str2html}}
+            {{.Body}}
         {{end}}
     </p>
     <hr>
@@ -228,7 +228,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
 
 > [@rhonda](#)(Rhonda Myers)更新了 [mike/stuff#38](#)。
 >
-> #### 消息内容:
+> #### 消息内容
 >
 > \_********************************\_********************************
 >
@@ -242,20 +242,20 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/
 
 模板系统包含一些函数,可用于进一步处理和格式化消息。以下是其中一些函数的列表:
 
-| 函数名            | 参数        | 可用于       | 用法                                                                              |
-| ----------------- | ----------- | ------------ | --------------------------------------------------------------------------------- |
-| `AppUrl`          | -           | 任何地方     | Gitea 的 URL                                                                     |
-| `AppName`         | -           | 任何地方     | 从 `app.ini` 中设置,通常为 "Gitea"                                               |
-| `AppDomain`       | -           | 任何地方     | Gitea 的主机名                                                                   |
-| `EllipsisString`  | string, int | 任何地方     | 将字符串截断为指定长度;根据需要添加省略号                                        |
-| `Str2html`        | string      | 仅正文部分   | 通过删除其中的 HTML 标签对文本进行清理                                              |
-| `Safe`            | string      | 仅正文部分   | 将输入作为 HTML 处理;可用于 `.ReviewComments.RenderedContent` 等字段               |
+| 函数名              | 参数        | 可用于       | 用法                             |
+|------------------| ----------- | ------------ | ------------------------------ |
+| `AppUrl`         | -           | 任何地方     | Gitea 的 URL                    |
+| `AppName`        | -           | 任何地方     | 从 `app.ini` 中设置,通常为 "Gitea"    |
+| `AppDomain`      | -           | 任何地方     | Gitea 的主机名                     |
+| `EllipsisString` | string, int | 任何地方     | 将字符串截断为指定长度;根据需要添加省略号          |
+| `SanitizeHTML`   | string      | 仅正文部分   | 通过删除其中的危险 HTML 标签对文本进行清理       |
+| `SafeHTML`       | string      | 仅正文部分   | 将输入作为 HTML 处理;可用于输出原始的 HTML 内容 |
 
 这些都是 _函数_,而不是元数据,因此必须按以下方式使用:
 
 ```html
-像这样使用:         {{Str2html "Escape<my>text"}}
-或者这样使用:       {{"Escape<my>text" | Str2html}}
+像这样使用:         {{SanitizeHTML "Escape<my>text"}}
+或者这样使用:       {{"Escape<my>text" | SanitizeHTML}}
 或者这样使用:       {{AppUrl}}
 但不要像这样使用:   {{.AppUrl}}
 ```
diff --git a/docs/content/administration/repo-indexer.en-us.md b/docs/content/administration/repo-indexer.en-us.md
index 6dec2d63fa..aa82222911 100644
--- a/docs/content/administration/repo-indexer.en-us.md
+++ b/docs/content/administration/repo-indexer.en-us.md
@@ -17,6 +17,12 @@ menu:
 
 # Repository indexer
 
+## Builtin repository code search without indexer
+
+Users could do repository-level code search without setting up a repository indexer.
+The builtin code search is based on the `git grep` command, which is fast and efficient for small repositories.
+Better code search support could be achieved by setting up the repository indexer.
+
 ## Setting up the repository indexer
 
 Gitea can search through the files of the repositories by enabling this function in your [`app.ini`](administration/config-cheat-sheet.md):
diff --git a/docs/content/contributing/guidelines-backend.en-us.md b/docs/content/contributing/guidelines-backend.en-us.md
index 084b3886e8..3159a5ff7d 100644
--- a/docs/content/contributing/guidelines-backend.en-us.md
+++ b/docs/content/contributing/guidelines-backend.en-us.md
@@ -101,6 +101,10 @@ i.e. `services/user`, `models/repository`.
 Since there are some packages which use the same package name, it is possible that you find packages like `modules/user`, `models/user`, and `services/user`. When these packages are imported in one Go file, it's difficult to know which package we are using and if it's a variable name or an import name. So, we always recommend to use import aliases. To differ from package variables which are commonly in camelCase, just use **snake_case** for import aliases.
 i.e. `import user_service "code.gitea.io/gitea/services/user"`
 
+### Implementing `io.Closer`
+
+If a type implements `io.Closer`, calling `Close` multiple times must not fail or `panic` but return an error or `nil`.
+
 ### Important Gotchas
 
 - Never write `x.Update(exemplar)` without an explicit `WHERE` clause:
diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md
index edd89e1231..efeaf38bb2 100644
--- a/docs/content/contributing/guidelines-frontend.en-us.md
+++ b/docs/content/contributing/guidelines-frontend.en-us.md
@@ -34,7 +34,7 @@ The source files can be found in the following directories:
 
 We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html) and [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html)
 
-### Gitea specific guidelines:
+### Gitea specific guidelines
 
 1. Every feature (Fomantic-UI/jQuery module) should be put in separate files/directories.
 2. HTML ids and classes should use kebab-case, it's preferred to contain 2-3 feature related keywords.
@@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h
 9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided.
 10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event.
 11. Custom event names are recommended to use `ce-` prefix.
-12. Gitea's tailwind-style CSS classes use `gt-` prefix (`gt-relative`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
+12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-word-break`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
 13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided.
 
 ### Accessibility / ARIA
@@ -118,7 +118,7 @@ However, there are still some special cases, so the current guideline is:
 ### Show/Hide Elements
 
 * Vue components are recommended to use `v-if` and `v-show` to show/hide elements.
-* Go template code should use Gitea's `.gt-hidden` and `showElem()/hideElem()/toggleElem()`, see more details in `.gt-hidden`'s comment.
+* Go template code should use `.tw-hidden` and `showElem()/hideElem()/toggleElem()`, see more details in `.tw-hidden`'s comment.
 
 ### Styles and Attributes in Go HTML Template
 
diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md
index 66a4d4b4d6..394097b259 100644
--- a/docs/content/contributing/guidelines-frontend.zh-cn.md
+++ b/docs/content/contributing/guidelines-frontend.zh-cn.md
@@ -34,7 +34,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
 
 我们推荐使用[Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html)和[Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html)。
 
-## Gitea 特定准则:
+## Gitea 特定准则
 
 1. 每个功能(Fomantic-UI/jQuery 模块)应放在单独的文件/目录中。
 2. HTML 的 id 和 class 应使用 kebab-case,最好包含2-3个与功能相关的关键词。
@@ -47,12 +47,13 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
 9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。
 10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。
 11. 推荐使用自定义事件名称前缀`ce-`。
-12. Gitea 的 tailwind-style CSS 类使用`gt-`前缀(`gt-relative`),而 Gitea 自身的私有框架级 CSS 类使用`g-`前缀(`g-modal-confirm`)。
+12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-word-break`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
+13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。
 
 ### 可访问性 / ARIA
 
 在历史上,Gitea大量使用了可访问性不友好的框架 Fomantic UI。
-Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`aria.md`),
+Gitea 使用一些补丁使 Fomantic UI 更具可访问性(参见 `aria.md`),
 但仍然存在许多问题需要大量的工作和时间来修复。
 
 ### 框架使用
@@ -64,18 +65,21 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari
 
 * Vue + Vanilla JS
 * Fomantic-UI(jQuery)
+* htmx (部分页面重新加载其他静态组件)
 * Vanilla JS
 
 不推荐的实现方式:
 
 * Vue + Fomantic-UI(jQuery)
 * jQuery + Vanilla JS
+* htmx + 任何其他需要大量 JavaScript 代码或不必要的功能,如 htmx 脚本 (`hx-on`)
 
 为了保持界面一致,Vue 组件可以使用 Fomantic-UI 的 CSS 类。
 尽管不建议混合使用不同的框架,
+我们使用 htmx 进行简单的交互。您可以在此 [PR](https://github.com/go-gitea/gitea/pull/28908) 中查看一个简单交互的示例,其中应使用 htmx。如果您需要更高级的反应性,请不要使用 htmx,请使用其他框架(Vue/Vanilla JS)。
 但如果混合使用是必要的,并且代码设计良好且易于维护,也可以工作。
 
-### async 函数
+### `async` 函数
 
 只有当函数内部存在`await`调用或返回`Promise`时,才将函数标记为`async`。
 
@@ -91,6 +95,12 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari
 这是有意为之的,我们想调用异步函数并忽略Promise。
 一些 lint 规则和 IDE 也会在未处理返回的 Promise 时发出警告。
 
+### 获取数据
+
+要获取数据,请使用`modules/fetch.js`中的包装函数`GET`、`POST`等。他们
+接受内容的`data`选项,将自动设置 CSRF 令牌并返回
+[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)。
+
 ### HTML 属性和 dataset
 
 禁止使用`dataset`,它的驼峰命名行为使得搜索属性变得困难。
@@ -107,7 +117,7 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari
 ### 显示/隐藏元素
 
 * 推荐在Vue组件中使用`v-if`和`v-show`来显示/隐藏元素。
-* Go 模板代码应使用 Gitea 的 `.gt-hidden` 和 `showElem()/hideElem()/toggleElem()` 来显示/隐藏元素,请参阅`.gt-hidden`的注释以获取更多详细信息。
+* Go 模板代码应使用 `.tw-hidden` 和 `showElem()/hideElem()/toggleElem()` 来显示/隐藏元素,请参阅`.tw-hidden`的注释以获取更多详细信息。
 
 ### Go HTML 模板中的样式和属性
 
@@ -132,3 +142,7 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari
 ### Vue3 和 JSX
 
 Gitea 现在正在使用 Vue3。我们决定不引入 JSX,以保持 HTML 代码和 JavaScript 代码分离。
+
+### UI示例
+
+Gitea 使用一些自制的 UI 元素并自定义其他元素,以将它们更好地集成到通用 UI 方法中。当在开发模式(`RUN_MODE=dev`)下运行 Gitea 时,在 `http(s)://your-gitea-url:port/devtest` 下会提供一个包含一些标准化 UI 示例的页面。
diff --git a/docs/content/development/api-usage.zh-cn.md b/docs/content/development/api-usage.zh-cn.md
index 96c1997294..d7aca16f7f 100644
--- a/docs/content/development/api-usage.zh-cn.md
+++ b/docs/content/development/api-usage.zh-cn.md
@@ -60,7 +60,7 @@ curl "http://localhost:4000/api/v1/repos/test1/test1/issues" \
 `/users/:name/tokens` 是一个特殊的接口,需要您使用 basic authentication 进行认证,具体原因在 issue 中
 [#3842](https://github.com/go-gitea/gitea/issues/3842#issuecomment-397743346) 有所提及,使用方法如下所示:
 
-### 使用 Basic authentication 认证:
+### 使用 Basic authentication 认证
 
 ```
 $ curl --url https://yourusername:yourpassword@gitea.your.host/api/v1/users/yourusername/tokens
diff --git a/docs/content/development/hacking-on-gitea.en-us.md b/docs/content/development/hacking-on-gitea.en-us.md
index 4b132c49d9..004e803827 100644
--- a/docs/content/development/hacking-on-gitea.en-us.md
+++ b/docs/content/development/hacking-on-gitea.en-us.md
@@ -214,7 +214,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200
 
 ### Building and adding SVGs
 
-SVG icons are built using the `make svg` target which compiles the icon sources defined in `build/generate-svg.js` into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory.
+SVG icons are built using the `make svg` target which compiles the icon sources into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory.
 
 ### Building the Logo
 
@@ -243,10 +243,10 @@ documentation using:
 make generate-swagger
 ```
 
-You should validate your generated Swagger file and spell-check it with:
+You should validate your generated Swagger file:
 
 ```bash
-make swagger-validate misspell-check
+make swagger-validate
 ```
 
 You should commit the changed swagger JSON file. The continuous integration
@@ -333,14 +333,9 @@ Documentation for the website is found in `docs/`. If you change this you
 can test your changes to ensure that they pass continuous integration using:
 
 ```bash
-# from the docs directory within Gitea
-make trans-copy clean build
+make lint-md
 ```
 
-You will require a copy of [Hugo](https://gohugo.io/) to run this task. Please
-note: this may generate a number of untracked Git objects, which will need to
-be cleaned up.
-
 ## Visual Studio Code
 
 A `launch.json` and `tasks.json` are provided within `contrib/ide/vscode` for
diff --git a/docs/content/development/hacking-on-gitea.zh-cn.md b/docs/content/development/hacking-on-gitea.zh-cn.md
index 364bbf1ffe..7dfea30538 100644
--- a/docs/content/development/hacking-on-gitea.zh-cn.md
+++ b/docs/content/development/hacking-on-gitea.zh-cn.md
@@ -201,7 +201,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200
 
 ### 构建和添加 SVGs
 
-SVG 图标是使用 `make svg` 目标构建的,该目标将 `build/generate-svg.js` 中定义的图标源编译到输出目录 `public/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。
+SVG 图标是使用 `make svg` 命令构建的,该命令将图标资源编译到输出目录 `public/assets/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。
 
 ### 构建 Logo
 
@@ -228,10 +228,10 @@ Gitea Logo的 PNG 和 SVG 版本是使用 `TAGS="gitea" make generate-images` 
 make generate-swagger
 ```
 
-您应该验证生成的 Swagger 文件并使用以下命令对其进行拼写检查:
+您应该验证生成的 Swagger 文件:
 
 ```bash
-make swagger-validate misspell-check
+make swagger-validate
 ```
 
 您应该提交更改后的 swagger JSON 文件。持续集成服务器将使用以下方法检查是否已完成:
@@ -307,13 +307,9 @@ TAGS="bindata sqlite sqlite_unlock_notify" make build test-sqlite
 该网站的文档位于 `docs/` 中。如果你改变了文档内容,你可以使用以下测试方法进行持续集成:
 
 ```bash
-# 来自 Gitea 中的 docs 目录
-make trans-copy clean build
+make lint-md
 ```
 
-运行此任务依赖于 [Hugo](https://gohugo.io/)。请注意:这可能会生成一些未跟踪的 Git 对象,
-需要被清理干净。
-
 ## Visual Studio Code
 
 `contrib/ide/vscode` 中为 Visual Studio Code 提供了 `launch.json` 和 `tasks.json`。查看
diff --git a/docs/content/help/faq.en-us.md b/docs/content/help/faq.en-us.md
index 5ea2c10f5e..b3b0980125 100644
--- a/docs/content/help/faq.en-us.md
+++ b/docs/content/help/faq.en-us.md
@@ -221,9 +221,11 @@ Our translations are currently crowd-sourced on our [Crowdin project](https://cr
 
 Whether you want to change a translation or add a new one, it will need to be there as all translations are overwritten in our CI via the Crowdin integration.
 
-## Push Hook / Webhook aren't running
+## Push Hook / Webhook / Actions aren't running
 
-If you can push but can't see push activities on the home dashboard, or the push doesn't trigger webhook, there are a few possibilities:
+If you can push but can't see push activities on the home dashboard, or the push doesn't trigger webhook and Actions workflows, it's likely that the git hooks are not working.
+
+There are a few possibilities:
 
 1. The git hooks are out of sync: run "Resynchronize pre-receive, update and post-receive hooks of all repositories" on the site admin panel
 2. The git repositories (and hooks) are stored on some filesystems (ex: mounted by NAS) which don't support script execution, make sure the filesystem supports `chmod a+x any-script`
diff --git a/docs/content/help/faq.zh-cn.md b/docs/content/help/faq.zh-cn.md
index b8dd3cd180..25230df70b 100644
--- a/docs/content/help/faq.zh-cn.md
+++ b/docs/content/help/faq.zh-cn.md
@@ -225,9 +225,11 @@ Gitea还提供了自己的SSH服务器,用于在SSHD不可用时使用。
 
 无论您想要更改翻译还是添加新的翻译,都需要在Crowdin集成中进行,因为所有翻译都会被CI覆盖。
 
-## 推送钩子/ Webhook未运行
+## 推送钩子/ Webhook / Actions 未运行
 
-如果您可以推送但无法在主页仪表板上看到推送活动,或者推送不触发Webhook,有几种可能性:
+如果您可以推送但无法在主页仪表板上看到推送活动,或者推送不触发 Webhook 和 Actions,可能是 git 钩子不工作而导致的。
+
+这可能是由于以下原因:
 
 1. Git钩子不同步:在站点管理面板上运行“重新同步所有仓库的pre-receive、update和post-receive钩子”
 2. Git仓库(和钩子)存储在一些不支持脚本执行的文件系统上(例如由NAS挂载),请确保文件系统支持`chmod a+x any-script`
diff --git a/docs/content/help/support.zh-cn.md b/docs/content/help/support.zh-cn.md
index de56d8abe0..91b37c586c 100644
--- a/docs/content/help/support.zh-cn.md
+++ b/docs/content/help/support.zh-cn.md
@@ -15,11 +15,64 @@ menu:
     identifier: "support"
 ---
 
-## 需要帮助?
+# 支持选项
 
-如果您在使用或者开发过程中遇到问题,请到以下渠道咨询:
+- [付费商业支持](https://about.gitea.com/)
+- [Discord](https://discord.gg/Gitea)
+- [Discourse 论坛](https://discourse.gitea.io/)
+- [Matrix](https://matrix.to/#/#gitea-space:matrix.org)
+  - 注意:大多数 Matrix 频道都与 Discord 中的对应频道桥接,可能在桥接过程中会出现一定程度的不稳定性。
+- 中文支持
+  - [Discourse 中文分类](https://discourse.gitea.io/c/5-category/5)
+  - QQ 群 328432459
 
-- 到 [GitHub Issue](https://github.com/go-gitea/gitea/issues) 提问(因为项目维护人员来自世界各地,为保证沟通顺畅,请使用英文提问)
-- 中文问题到 [Gitea 论坛](https://discourse.gitea.io/c/5-category/5) 提问
-- 访问 [Discord Gitea 聊天室 - 英文](https://discord.gg/Gitea)
-- 加入 QQ群 328432459 获得进一步的支持
+# Bug 报告
+
+如果您发现了 Bug,请在 GitHub 上 [创建一个问题](https://github.com/go-gitea/gitea/issues)。
+
+**注意:** 在请求支持时,可能需要准备以下信息,以便帮助者获得所需的所有信息:
+
+1. 您的 `app.ini`(将任何敏感数据进行必要的清除)。
+2. 您看到的任何错误消息。
+3. Gitea 日志以及与情况相关的所有其他日志。
+   - 收集 `trace` / `debug` 级别的日志更有用(参见下一节)。
+   - 在使用 systemd 时,使用 `journalctl --lines 1000 --unit gitea` 收集日志。
+   - 在使用 Docker 时,使用 `docker logs --tail 1000 <gitea-container>` 收集日志。
+4. 可重现的步骤,以便他人能够更快速、更容易地重现和理解问题。
+   - [try.gitea.io](https://try.gitea.io) 可用于重现问题。
+5. 如果遇到慢速/挂起/死锁等问题,请在出现问题时报告堆栈跟踪。
+   转到 "Site Admin" -> "Monitoring" -> "Stacktrace" -> "Download diagnosis report"。
+
+# 高级 Bug 报告提示
+
+## 更多日志的配置选项
+
+默认情况下,日志以 `info` 级别输出到控制台。
+如果您需要设置日志级别和/或从文件中收集日志,
+您只需将以下配置复制到您的 `app.ini` 中(删除所有其他 `[log]` 部分),
+然后您将在 Gitea 的日志目录中找到 `*.log` 文件(默认为 `%(GITEA_WORK_DIR)/log`)。
+
+```ini
+; 要显示所有 SQL 日志,您还可以在 [database] 部分中设置 LOG_SQL=true
+[log]
+LEVEL=debug
+MODE=console,file
+```
+
+## 使用命令行收集堆栈跟踪
+
+Gitea 可以使用 Golang 的 pprof 处理程序和工具链来收集堆栈跟踪和其他运行时信息。
+
+如果 Web UI 停止工作,您可以尝试通过命令行收集堆栈跟踪:
+
+1. 设置 app.ini:
+
+    ```
+    [server]
+    ENABLE_PPROF = true
+    ```
+
+2. 重新启动 Gitea
+
+3. 尝试触发bug,当请求卡住一段时间,使用或浏览器访问:获取堆栈跟踪。
+`curl http://127.0.0.1:6060/debug/pprof/goroutine?debug=1`
diff --git a/docs/content/installation/comparison.en-us.md b/docs/content/installation/comparison.en-us.md
index 1ba4f7ecc2..3fb6561f31 100644
--- a/docs/content/installation/comparison.en-us.md
+++ b/docs/content/installation/comparison.en-us.md
@@ -87,6 +87,9 @@ _Symbols used in table:_
 | Git Blame                                   | ✓                                                   | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | ✓            |
 | Visual comparison of image changes          | ✓                                                   | ✘    | ✓         | ?         | ?         | ?         | ✘            | ✘            |
 
+- Gitea has builtin repository-level code search
+- Better code search support could be achieved by [using a repository indexer](administration/repo-indexer.md)
+
 ## Issue Tracker
 
 | Feature                       | Gitea                                               | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | RhodeCode EE |
diff --git a/docs/content/installation/database-preparation.zh-cn.md b/docs/content/installation/database-preparation.zh-cn.md
index d651088395..3fde004a8c 100644
--- a/docs/content/installation/database-preparation.zh-cn.md
+++ b/docs/content/installation/database-preparation.zh-cn.md
@@ -17,7 +17,9 @@ menu:
 
 # 数据库准备
 
-在使用 Gitea 前,您需要准备一个数据库。Gitea 支持 PostgreSQL(>= 12)、MySQL(>= 8.0)、SQLite 和 MSSQL(>= 2012 SP4)这几种数据库。本页将指导您准备数据库。由于 PostgreSQL 和 MySQL 在生产环境中被广泛使用,因此本文档将仅涵盖这两种数据库。如果您计划使用 SQLite,则可以忽略本章内容。
+在使用 Gitea 前,您需要准备一个数据库。Gitea 支持 PostgreSQL(>= 12)、MySQL(>= 8.0)、MariaDB(>= 10.4)、SQLite(内置) 和 MSSQL(>= 2012 SP4)这几种数据库。本页将指导您准备数据库。由于 PostgreSQL 和 MySQL 在生产环境中被广泛使用,因此本文档将仅涵盖这两种数据库。如果您计划使用 SQLite,则可以忽略本章内容。
+
+如果您使用不受支持的数据库版本,请通过 [联系我们](/help/support) 以获取有关我们的扩展支持的信息。我们可以为旧数据库提供测试和支持,并将这些修复集成到 Gitea 代码库中。
 
 数据库实例可以与 Gitea 实例在相同机器上(本地数据库),也可以与 Gitea 实例在不同机器上(远程数据库)。
 
@@ -61,7 +63,9 @@ menu:
 
 4. 使用 UTF-8 字符集和大小写敏感的排序规则创建数据库。
 
-    Gitea 启动后会尝试把数据库修改为更合适的字符集,如果你想指定自己的字符集规则,可以在 app.ini 中设置 `[database].CHARSET_COLLATION`。
+    `utf8mb4_bin` 是 MySQL/MariaDB 的通用排序规则。
+    Gitea 启动后会尝试把数据库修改为更合适的字符集 (`utf8mb4_0900_as_cs` 或者 `uca1400_as_cs`) 并在可能的情况下更改数据库。
+    如果你想指定自己的字符集规则,可以在 `app.ini` 中设置 `[database].CHARSET_COLLATION`。
 
     ```sql
     CREATE DATABASE giteadb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_bin';
@@ -85,7 +89,7 @@ menu:
     FLUSH PRIVILEGES;
     ```
 
-6. 通过 exit 退出数据库控制台。
+6. 通过 `exit` 退出数据库控制台。
 
 7. 在您的 Gitea 服务器上,测试与数据库的连接:
 
@@ -93,13 +97,13 @@ menu:
     mysql -u gitea -h 203.0.113.3 -p giteadb
     ```
 
-    其中 `gitea` 是数据库用户名,`giteadb` 是数据库名称,`203.0.113.3` 是数据库实例的 IP 地址。对于本地数据库,省略 -h 选项。
+    其中 `gitea` 是数据库用户名,`giteadb` 是数据库名称,`203.0.113.3` 是数据库实例的 IP 地址。对于本地数据库,省略 `-h` 选项。
 
     到此您应该能够连接到数据库了。
 
 ## PostgreSQL
 
-1. 对于远程数据库设置,通过编辑数据库实例上的 postgresql.conf 文件中的 listen_addresses 将 PostgreSQL 配置为监听您的 IP 地址:
+1. 对于远程数据库设置,通过编辑数据库实例上的 postgresql.conf 文件中的 `listen_addresses` 将 `PostgreSQL` 配置为监听您的 IP 地址:
 
     ```ini
     listen_addresses = 'localhost, 203.0.113.3'
diff --git a/docs/content/installation/from-source.en-us.md b/docs/content/installation/from-source.en-us.md
index 601e074745..cd9fd56511 100644
--- a/docs/content/installation/from-source.en-us.md
+++ b/docs/content/installation/from-source.en-us.md
@@ -27,13 +27,7 @@ Next, [install Node.js with npm](https://nodejs.org/en/download/) which is
 required to build the JavaScript and CSS files. The minimum supported Node.js
 version is @minNodeVersion@ and the latest LTS version is recommended.
 
-**Note**: When executing make tasks that require external tools, like
-`make misspell-check`, Gitea will automatically download and build these as
-necessary. To be able to use these, you must have the `"$GOPATH/bin"` directory
-on the executable path. If you don't add the go bin directory to the
-executable path, you will have to manage this yourself.
-
-**Note 2**: Go version @minGoVersion@ or higher is required. However, it is recommended to
+**Note**: Go version @minGoVersion@ or higher is required. However, it is recommended to
 obtain the same version as our continuous integration, see the advice given in
 [Hacking on Gitea](development/hacking-on-gitea.md)
 
diff --git a/docs/content/installation/from-source.zh-cn.md b/docs/content/installation/from-source.zh-cn.md
index c2bd5785b2..3ff7efb4ed 100644
--- a/docs/content/installation/from-source.zh-cn.md
+++ b/docs/content/installation/from-source.zh-cn.md
@@ -21,9 +21,7 @@ menu:
 
 接下来,[安装 Node.js 和 npm](https://nodejs.org/zh-cn/download/), 这是构建 JavaScript 和 CSS 文件所需的。最低支持的 Node.js 版本是 @minNodeVersion@,建议使用最新的 LTS 版本。
 
-**注意**:当执行需要外部工具的 make 任务(如`make misspell-check`)时,Gitea 将根据需要自动下载和构建这些工具。为了能够实现这个目的,你必须将`"$GOPATH/bin"`目录添加到可执行路径中。如果没有将 Go 的二进制目录添加到可执行路径中,你需要自行解决产生的问题。
-
-**注意2**:需要 Go 版本 @minGoVersion@ 或更高版本。不过,建议获取与我们的持续集成(continuous integration, CI)相同的版本,请参阅在 [Hacking on Gitea](development/hacking-on-gitea.md) 中给出的建议。
+**注意**:需要 Go 版本 @minGoVersion@ 或更高版本。不过,建议获取与我们的持续集成(continuous integration, CI)相同的版本,请参阅在 [Hacking on Gitea](development/hacking-on-gitea.md) 中给出的建议。
 
 ## 下载
 
diff --git a/docs/content/installation/windows-service.zh-cn.md b/docs/content/installation/windows-service.zh-cn.md
index 985492b7e8..d5761d2c19 100644
--- a/docs/content/installation/windows-service.zh-cn.md
+++ b/docs/content/installation/windows-service.zh-cn.md
@@ -15,7 +15,7 @@ menu:
     identifier: "windows-service"
 ---
 
-# 准备工作
+## 准备工作
 
 在 C:\gitea\custom\conf\app.ini 中进行了以下更改:
 
@@ -27,7 +27,7 @@ RUN_USER = COMPUTERNAME$
 
 COMPUTERNAME 是从命令行中运行 `echo %COMPUTERNAME%` 后得到的响应。如果响应是 `USER-PC`,那么 `RUN_USER = USER-PC$`。
 
-## 使用绝对路径
+### 使用绝对路径
 
 如果您使用 SQLite3,请将 `PATH` 更改为包含完整路径:
 
@@ -36,7 +36,7 @@ COMPUTERNAME 是从命令行中运行 `echo %COMPUTERNAME%` 后得到的响应
 PATH     = c:/gitea/data/gitea.db
 ```
 
-# 注册为Windows服务
+## 注册为Windows服务
 
 要注册为Windows服务,首先以Administrator身份运行 `cmd`,然后执行以下命令:
 
@@ -48,7 +48,16 @@ sc.exe create gitea start= auto binPath= "\"C:\gitea\gitea.exe\" web --config \"
 
 之后在控制面板打开 "Windows Services",搜索 "gitea",右键选择 "Run"。在浏览器打开 `http://localhost:3000` 就可以访问了。(如果你修改了端口,请访问对应的端口,3000是默认端口)。
 
-## 添加启动依赖项
+### 服务启动类型
+
+据观察,在启动期间加载的系统上,Gitea 服务可能无法启动,并在 Windows 事件日志中记录超时。
+在这种情况下,将启动类型更改为`Automatic-Delayed`。这可以在服务创建期间完成,或者通过运行配置命令来完成。
+
+```
+sc.exe config gitea start= delayed-auto
+```
+
+### 添加启动依赖项
 
 要将启动依赖项添加到 Gitea Windows 服务(例如 Mysql、Mariadb),作为管理员,然后运行以下命令:
 
diff --git a/docs/content/installation/with-docker.en-us.md b/docs/content/installation/with-docker.en-us.md
index e67f5bccb2..a16d4a8d60 100644
--- a/docs/content/installation/with-docker.en-us.md
+++ b/docs/content/installation/with-docker.en-us.md
@@ -304,7 +304,8 @@ services:
       - GITEA__mailer__ENABLED=true
       - GITEA__mailer__FROM=${GITEA__mailer__FROM:?GITEA__mailer__FROM not set}
       - GITEA__mailer__PROTOCOL=smtps
-      - GITEA__mailer__HOST=${GITEA__mailer__HOST:?GITEA__mailer__HOST not set}
+      - GITEA__mailer__SMTP_ADDR=${GITEA__mailer__SMTP_ADDR:?GITEA__mailer__SMTP_ADDR not set}
+      - GITEA__mailer__SMTP_PORT=${GITEA__mailer__SMTP_PORT:?GITEA__mailer__SMTP_PORT not set}
       - GITEA__mailer__USER=${GITEA__mailer__USER:-apikey}
       - GITEA__mailer__PASSWD="""${GITEA__mailer__PASSWD:?GITEA__mailer__PASSWD not set}"""
 ```
@@ -545,7 +546,7 @@ In this option, the idea is that the host SSH uses an `AuthorizedKeysCommand` in
   ```bash
   cat <<"EOF" | sudo tee /home/git/docker-shell
   #!/bin/sh
-  /usr/bin/docker exec -i --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@"
+  /usr/bin/docker exec -i -u git --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@"
   EOF
   sudo chmod +x /home/git/docker-shell
   sudo usermod -s /home/git/docker-shell git
@@ -560,7 +561,7 @@ Add the following block to `/etc/ssh/sshd_config`, on the host:
 ```bash
 Match User git
   AuthorizedKeysCommandUser git
-  AuthorizedKeysCommand /usr/bin/docker exec -i gitea /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k
+  AuthorizedKeysCommand /usr/bin/docker exec -i -u git gitea /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k
 ```
 
 (From 1.16.0 you will not need to set the `-c /data/gitea/conf/app.ini` option.)
diff --git a/docs/content/usage/actions/act-runner.en-us.md b/docs/content/usage/actions/act-runner.en-us.md
index 3fad9cbfe8..942d126919 100644
--- a/docs/content/usage/actions/act-runner.en-us.md
+++ b/docs/content/usage/actions/act-runner.en-us.md
@@ -120,6 +120,8 @@ A registration token can also be obtained from the gitea [command-line interface
 gitea --config /etc/gitea/app.ini actions generate-runner-token
 ```
 
+Tokens are valid for registering multiple runners, until they are revoked and replaced by a new token using the token reset link in the web interface.
+
 ### Register the runner
 
 The act runner can be registered by running the following command:
@@ -301,34 +303,3 @@ sudo systemctl enable act_runner --now
 ```
 
 If using Docker, the `act_runner` user should also be added to the `docker` group before starting the service. Keep in mind that this effectively gives `act_runner` root access to the system [[1]](https://docs.docker.com/engine/security/#docker-daemon-attack-surface).
-
-## Configuration variable
-
-You can create configuration variables on the user, organization and repository level.
-The level of the variable depends on where you created it.
-
-### Naming conventions
-
-The following rules apply to variable names:
-
-- Variable names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed.
-
-- Variable names must not start with the `GITHUB_` and `GITEA_` prefix.
-
-- Variable names must not start with a number.
-
-- Variable names are case-insensitive.
-
-- Variable names must be unique at the level they are created at.
-
-- Variable names must not be `CI`.
-
-### Using variable
-
-After creating configuration variables, they will be automatically filled in the `vars` context.
-They can be accessed through expressions like `{{ vars.VARIABLE_NAME }}` in the workflow.
-
-### Precedence
-
-If a variable with the same name exists at multiple levels, the variable at the lowest level takes precedence:
-A repository variable will always be chosen over an organization/user variable.
diff --git a/docs/content/usage/actions/act-runner.zh-cn.md b/docs/content/usage/actions/act-runner.zh-cn.md
index 274b0f0692..e5ebff976d 100644
--- a/docs/content/usage/actions/act-runner.zh-cn.md
+++ b/docs/content/usage/actions/act-runner.zh-cn.md
@@ -258,32 +258,3 @@ Runner的标签用于确定Runner可以运行哪些Job以及如何运行它们
 Runner将从Gitea实例获取Job并自动运行它们。
 
 由于Act Runner仍处于开发中,建议定期检查最新版本并进行升级。
-
-## 变量
-
-您可以创建用户、组织和仓库级别的变量。变量的级别取决于创建它的位置。
-
-### 命名规则
-
-以下规则适用于变量名:
-
-- 变量名称只能包含字母数字字符 (`[a-z]`, `[A-Z]`, `[0-9]`) 或下划线 (`_`)。不允许使用空格。
-
-- 变量名称不能以 `GITHUB_` 和 `GITEA_` 前缀开头。
-
-- 变量名称不能以数字开头。
-
-- 变量名称不区分大小写。
-
-- 变量名称在创建它们的级别上必须是唯一的。
-
-- 变量名称不能为 “CI”。
-
-### 使用
-
-创建配置变量后,它们将自动填充到 `vars` 上下文中。您可以在工作流中使用类似 `{{ vars.VARIABLE_NAME }}` 这样的表达式来使用它们。
-
-### 优先级
-
-如果同名变量存在于多个级别,则级别最低的变量优先。
-仓库级别的变量总是比组织或者用户级别的变量优先被选中。
diff --git a/docs/content/usage/actions/comparison.zh-cn.md b/docs/content/usage/actions/comparison.zh-cn.md
index dbe9ca007d..16b2181ba2 100644
--- a/docs/content/usage/actions/comparison.zh-cn.md
+++ b/docs/content/usage/actions/comparison.zh-cn.md
@@ -95,12 +95,6 @@ Gitea Actions目前不支持此功能,如果使用它,结果将始终为空
 
 ## 缺失的功能
 
-### 变量
-
-请参阅[变量](https://docs.github.com/zh/actions/learn-github-actions/variables)。
-
-目前变量功能正在开发中。
-
 ### 问题匹配器
 
 问题匹配器是一种扫描Actions输出以查找指定正则表达式模式并在用户界面中突出显示该信息的方法。
diff --git a/docs/content/usage/actions/design.en-us.md b/docs/content/usage/actions/design.en-us.md
index 29fa433e59..0d72c19dce 100644
--- a/docs/content/usage/actions/design.en-us.md
+++ b/docs/content/usage/actions/design.en-us.md
@@ -104,7 +104,7 @@ However, if a job container tries to fetch code from localhost, it will fail bec
 ### Connection 3, act runner to internet
 
 When you use some actions like `actions/checkout@v4`, the act runner downloads the scripts, not the job containers.
-By default, it downloads from [gitea.com](http://gitea.com/), so it requires access to the internet.
+By default, it downloads from [github.com](http://github.com/), so it requires access to the internet. If you configure the `DEFAULT_ACTIONS_URL` to `self`, then it will download from your Gitea instance by default. Then it will not connect to internet when downloading the action itself.
 It also downloads some docker images from Docker Hub by default, which also requires internet access.
 
 However, internet access is not strictly necessary.
diff --git a/docs/content/usage/actions/design.zh-cn.md b/docs/content/usage/actions/design.zh-cn.md
index 8add1cf7c5..f48576477f 100644
--- a/docs/content/usage/actions/design.zh-cn.md
+++ b/docs/content/usage/actions/design.zh-cn.md
@@ -105,7 +105,8 @@ act runner 必须能够连接到Gitea以接收任务并发送执行结果回来
 ### 连接 3,act runner到互联网
 
 当您使用诸如 `actions/checkout@v4` 的一些Actions时,act runner下载的是脚本,而不是Job容器。
-默认情况下,它从[gitea.com](http://gitea.com/)下载,因此需要访问互联网。
+默认情况下,它从[github.com](http://github.com/)下载,因此需要访问互联网。如果您设置的是 self,
+那么默认将从您的当前Gitea实例下载,那么此步骤不需要连接到互联网。
 它还默认从Docker Hub下载一些Docker镜像,这也需要互联网访问。
 
 然而,互联网访问并不是绝对必需的。
diff --git a/docs/content/usage/actions/faq.en-us.md b/docs/content/usage/actions/faq.en-us.md
index 7ed59e02cd..427d57c43e 100644
--- a/docs/content/usage/actions/faq.en-us.md
+++ b/docs/content/usage/actions/faq.en-us.md
@@ -45,25 +45,24 @@ It is technically possible to implement, but we need to discuss whether it is ne
 
 ## Where will the runner download scripts when using actions such as `actions/checkout@v4`?
 
-You may be aware that there are tens of thousands of [marketplace actions](https://github.com/marketplace?type=actions) in GitHub.
-However, when you write `uses: actions/checkout@v4`, it actually downloads the scripts from [gitea.com/actions/checkout](http://gitea.com/actions/checkout) by default (not GitHub).
-This is a mirror of [github.com/actions/checkout](http://github.com/actions/checkout), but it's impossible to mirror all of them.
-That's why you may encounter failures when trying to use some actions that haven't been mirrored.
+There are tens of thousands of [actions scripts](https://github.com/marketplace?type=actions) in GitHub, and when you write `uses: actions/checkout@v4`, it downloads the scripts from [github.com/actions/checkout](http://github.com/actions/checkout) by default.
+But what if you want to use actions from other places such as gitea.com instead of GitHub?
 
 The good news is that you can specify the URL prefix to use actions from anywhere.
 This is an extra syntax in Gitea Actions.
 For example:
 
-- `uses: https://github.com/xxx/xxx@xxx`
 - `uses: https://gitea.com/xxx/xxx@xxx`
+- `uses: https://github.com/xxx/xxx@xxx`
 - `uses: http://your_gitea_instance.com/xxx@xxx`
 
 Be careful, the `https://` or `http://` prefix is necessary!
 
-Alternatively, if you want your runners to download actions from GitHub or your own Gitea instance by default, you can configure it by setting `[actions].DEFAULT_ACTIONS_URL`.
-See [Configuration Cheat Sheet](administration/config-cheat-sheet.md#actions-actions).
+This is one of the differences from GitHub Actions which supports actions scripts only from GitHub.
+But it should allow users much more flexibility in how they run Actions.
 
-This is one of the differences from GitHub Actions, but it should allow users much more flexibility in how they run Actions.
+Alternatively, if you want your runners to download actions from your own Gitea instance by default, you can configure it by setting `[actions].DEFAULT_ACTIONS_URL`.
+See [Configuration Cheat Sheet](administration/config-cheat-sheet.md#actions-actions).
 
 ## How to limit the permission of the runners?
 
diff --git a/docs/content/usage/actions/faq.zh-cn.md b/docs/content/usage/actions/faq.zh-cn.md
index ba5f87bf0c..d6e1466801 100644
--- a/docs/content/usage/actions/faq.zh-cn.md
+++ b/docs/content/usage/actions/faq.zh-cn.md
@@ -45,25 +45,25 @@ DEFAULT_REPO_UNITS = ...,repo.actions
 
 ## 使用`actions/checkout@v4`等Actions时,Job容器会从何处下载脚本?
 
-您可能知道GitHub上有成千上万个[Actions市场](https://github.com/marketplace?type=actions)。
-然而,当您编写`uses: actions/checkout@v4`时,它实际上默认从[gitea.com/actions/checkout](http://gitea.com/actions/checkout)下载脚本(而不是从GitHub下载)。
-这是[github.com/actions/checkout](http://github.com/actions/checkout)的镜像,但无法将它们全部镜像。
-这就是为什么在尝试使用尚未镜像的某些Actions时可能会遇到失败的原因。
+GitHub 上有成千上万个 [Actions 脚本](https://github.com/marketplace?type=actions)。
+当您编写 `uses: actions/checkout@v4` 时,它默认会从 [github.com/actions/checkout](https://github.com/actions/checkout) 下载脚本。
+那如果您想使用一些托管在其它平台上的脚本呢,比如在 gitea.com 上的?
 
 好消息是,您可以指定要从任何位置使用Actions的URL前缀。
 这是Gitea Actions中的额外语法。
 例如:
 
-- `uses: https://github.com/xxx/xxx@xxx`
 - `uses: https://gitea.com/xxx/xxx@xxx`
+- `uses: https://github.com/xxx/xxx@xxx`
 - `uses: http://your_gitea_instance.com/xxx@xxx`
 
 注意,`https://`或`http://`前缀是必需的!
 
-另外,如果您希望您的Runner默认从GitHub或您自己的Gitea实例下载Actions,可以通过设置 `[actions].DEFAULT_ACTIONS_URL`进行配置。
-参见[配置速查表](administration/config-cheat-sheet.md#actions-actions)。
+这是与 GitHub Actions 的一个区别,GitHub Actions 只允许使用托管在 GitHub 上的 actions 脚本。
+但用户理应拥有权利去灵活决定如何运行 Actions。
 
-这是与GitHub Actions的一个区别,但它应该允许用户以更灵活的方式运行Actions。
+另外,如果您希望您的 Runner 默认从您自己的 Gitea 实例下载 Actions,可以通过设置 `[actions].DEFAULT_ACTIONS_URL`进行配置。
+参见[配置速查表](administration/config-cheat-sheet.md#actions-actions)。
 
 ## 如何限制Runner的权限?
 
diff --git a/docs/content/usage/actions/quickstart.en-us.md b/docs/content/usage/actions/quickstart.en-us.md
index 2a2cf72584..0514b6ddf2 100644
--- a/docs/content/usage/actions/quickstart.en-us.md
+++ b/docs/content/usage/actions/quickstart.en-us.md
@@ -61,8 +61,8 @@ It is always a bad idea to use a loopback address such as `127.0.0.1` or `localh
 If you are unsure which address to use, the LAN address is usually the right choice.
 
 `token` is used for authentication and identification, such as `P2U1U0oB4XaRCi8azcngmPCLbRpUGapalhmddh23`.
-It is one-time use only and cannot be used to register multiple runners.
-You can obtain different levels of 'tokens' from the following places to create the corresponding level of' runners':
+Each token can be used to create multiple runners, until it is replaced with a new token using the reset link.
+You can obtain different levels of 'tokens' from the following places to create the corresponding level of 'runners':
 
 - Instance level: The admin settings page, like `<your_gitea.com>/admin/actions/runners`.
 - Organization level: The organization settings page, like `<your_gitea.com>/<org>/settings/actions/runners`.
diff --git a/docs/content/usage/actions/variables.en-us.md b/docs/content/usage/actions/variables.en-us.md
new file mode 100644
index 0000000000..dee2e74234
--- /dev/null
+++ b/docs/content/usage/actions/variables.en-us.md
@@ -0,0 +1,41 @@
+---
+date: "2024-04-10T22:21:00+08:00"
+title: "Variables"
+slug: "actions-variables"
+sidebar_position: 25
+draft: false
+toc: false
+menu:
+  sidebar:
+    parent: "actions"
+    name: "Variables"
+    sidebar_position: 25
+    identifier: "actions-variables"
+---
+
+## Variables
+
+You can create configuration variables on the user, organization and repository level.
+The level of the variable depends on where you created it. When creating a variable, the
+key will be converted to uppercase. You need use uppercase on the yaml file.
+
+### Naming conventions
+
+The following rules apply to variable names:
+
+- Variable names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed.
+- Variable names must not start with the `GITHUB_` and `GITEA_` prefix.
+- Variable names must not start with a number.
+- Variable names are case-insensitive.
+- Variable names must be unique at the level they are created at.
+- Variable names must not be `CI`.
+
+### Using variable
+
+After creating configuration variables, they will be automatically filled in the `vars` context.
+They can be accessed through expressions like `${{ vars.VARIABLE_NAME }}` in the workflow.
+
+### Precedence
+
+If a variable with the same name exists at multiple levels, the variable at the lowest level takes precedence:
+A repository variable will always be chosen over an organization/user variable.
diff --git a/docs/content/usage/actions/variables.zh-cn.md b/docs/content/usage/actions/variables.zh-cn.md
new file mode 100644
index 0000000000..77643408a1
--- /dev/null
+++ b/docs/content/usage/actions/variables.zh-cn.md
@@ -0,0 +1,39 @@
+---
+date: "2024-04-10T22:21:00+08:00"
+title: "变量"
+slug: "actions-variables"
+sidebar_position: 25
+draft: false
+toc: false
+menu:
+  sidebar:
+    parent: "actions"
+    name: "变量"
+    sidebar_position: 25
+    identifier: "actions-variables"
+---
+
+## 变量
+
+您可以创建用户、组织和仓库级别的变量。变量的级别取决于创建它的位置。当创建变量时,变量的名称会被
+转换为大写,在yaml文件中引用时需要使用大写。
+
+### 命名规则
+
+以下规则适用于变量名:
+
+- 变量名称只能包含字母数字字符 (`[a-z]`, `[A-Z]`, `[0-9]`) 或下划线 (`_`)。不允许使用空格。
+- 变量名称不能以 `GITHUB_` 和 `GITEA_` 前缀开头。
+- 变量名称不能以数字开头。
+- 变量名称不区分大小写。
+- 变量名称在创建它们的级别上必须是唯一的。
+- 变量名称不能为 `CI`。
+
+### 使用
+
+创建配置变量后,它们将自动填充到 `vars` 上下文中。您可以在工作流中使用类似 `${{ vars.VARIABLE_NAME }}` 这样的表达式来使用它们。
+
+### 优先级
+
+如果同名变量存在于多个级别,则级别最低的变量优先。
+仓库级别的变量总是比组织或者用户级别的变量优先被选中。
diff --git a/docs/content/usage/badge.en-us.md b/docs/content/usage/badge.en-us.md
new file mode 100644
index 0000000000..212134e01c
--- /dev/null
+++ b/docs/content/usage/badge.en-us.md
@@ -0,0 +1,37 @@
+---
+date: "2023-02-25T00:00:00+00:00"
+title: "Badge"
+slug: "badge"
+sidebar_position: 11
+toc: false
+draft: false
+aliases:
+  - /en-us/badge
+menu:
+  sidebar:
+    parent: "usage"
+    name: "Badge"
+    sidebar_position: 11
+    identifier: "Badge"
+---
+
+# Badge
+
+Gitea has its builtin Badge system which allows you to display the status of your repository in other places. You can use the following badges:
+
+## Workflow Badge
+
+The Gitea Actions workflow badge is a badge that shows the status of the latest workflow run.
+It is designed to be compatible with [GitHub Actions workflow badge](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/adding-a-workflow-status-badge).
+
+You can use the following URL to get the badge:
+
+```
+https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}?branch={branch}&event={event}
+```
+
+- `{owner}`: The owner of the repository.
+- `{repo}`: The name of the repository.
+- `{workflow_file}`: The name of the workflow file.
+- `{branch}`: Optional. The branch of the workflow. Default to your repository's default branch.
+- `{event}`: Optional. The event of the workflow. Default to none.
diff --git a/docs/content/usage/blocking-users.en-us.md b/docs/content/usage/blocking-users.en-us.md
new file mode 100644
index 0000000000..b59bbe4d62
--- /dev/null
+++ b/docs/content/usage/blocking-users.en-us.md
@@ -0,0 +1,56 @@
+---
+date: "2024-01-31T00:00:00+00:00"
+title: "Blocking a user"
+slug: "blocking-user"
+sidebar_position: 25
+toc: false
+draft: false
+aliases:
+  - /en-us/webhooks
+menu:
+  sidebar:
+    parent: "usage"
+    name: "Blocking a user"
+    sidebar_position: 30
+    identifier: "blocking-user"
+---
+
+# Blocking a user
+
+Gitea supports blocking of users to restrict how they can interact with you and your content.
+
+You can block a user in your account settings, from the user's profile or from comments created by the user.
+The user is not directly notified about the block, but they can notice they are blocked when they attempt to interact with you.
+Organization owners can block anyone who is not a member of the organization too.
+If a blocked user has admin permissions, they can still perform all actions even if blocked.
+
+### When you block a user
+
+- the user stops following you
+- you stop following the user
+- the user's stars are removed from your repositories
+- your stars are removed from their repositories
+- the user stops watching your repositories
+- you stop watching their repositories
+- the user's issue assignments are removed from your repositories
+- your issue assignments are removed from their repositories
+- the user is removed as a collaborator on your repositories
+- you are removed as a collaborator on their repositories
+- any pending repository transfers to or from the blocked user are canceled
+
+### When you block a user, the user cannot
+
+- follow you
+- watch your repositories
+- star your repositories
+- fork your repositories
+- transfer repositories to you
+- open issues or pull requests on your repositories
+- comment on issues or pull requests you've created
+- comment on issues or pull requests on your repositories
+- react to your comments on issues or pull requests
+- react to comments on issues or pull requests on your repositories
+- assign you to issues or pull requests
+- add you as a collaborator on their repositories
+- send you notifications by @mentioning your username
+- be added as team member (if blocked by an organization)
diff --git a/docs/content/usage/issue-pull-request-templates.en-us.md b/docs/content/usage/issue-pull-request-templates.en-us.md
index 34475e3465..e203c0d379 100644
--- a/docs/content/usage/issue-pull-request-templates.en-us.md
+++ b/docs/content/usage/issue-pull-request-templates.en-us.md
@@ -19,9 +19,10 @@ menu:
 
 Some projects have a standard list of questions that users need to answer
 when creating an issue or pull request. Gitea supports adding templates to the
-main branch of the repository so that they can autopopulate the form when users are
+**default branch of the repository** so that they can autopopulate the form when users are
 creating issues and pull requests. This will cut down on the initial back and forth
 of getting some clarifying details.
+It is currently not possible to provide generic issue/pull-request templates globally.
 
 Additionally, the New Issue page URL can be suffixed with `?title=Issue+Title&body=Issue+Text` and the form will be populated with those strings. Those strings will be used instead of the template if there is one.
 
@@ -135,6 +136,12 @@ body:
     attributes:
       value: |
         Thanks for taking the time to fill out this bug report!
+  # some markdown that will only be visible once the issue has been created
+  - type: markdown
+    attributes:
+      value: |
+        This issue was created by an issue **template** :)
+    visible: [content]
   - type: input
     id: contact
     attributes:
@@ -186,11 +193,16 @@ body:
       options:
         - label: I agree to follow this project's Code of Conduct
           required: true
+        - label: I have also read the CONTRIBUTION.MD
+          required: true
+          visible: [form]
+        - label: This is a TODO only visible after issue creation
+          visible: [content]
 ```
 
 ### Markdown
 
-You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted.
+You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted by default.
 
 Attributes:
 
@@ -198,6 +210,8 @@ Attributes:
 |-------|--------------------------------------------------------------|----------|--------|---------|--------------|
 | value | The text that is rendered. Markdown formatting is supported. | Required | String | -       | -            |
 
+visible: Default is **[form]**
+
 ### Textarea
 
 You can use a `textarea` element to add a multi-line text field to your form. Contributors can also attach files in `textarea` fields.
@@ -218,6 +232,8 @@ Validations:
 |----------|------------------------------------------------------|----------|---------|---------|--------------|
 | required | Prevents form submission until element is completed. | Optional | Boolean | false   | -            |
 
+visible: Default is **[form, content]**
+
 ### Input
 
 You can use an `input` element to add a single-line text field to your form.
@@ -239,6 +255,8 @@ Validations:
 | is_number | Prevents form submission until element is filled with a number.                                  | Optional | Boolean | false   | -                                                                        |
 | regex     | Prevents form submission until element is filled with a value that match the regular expression. | Optional | String  | -       | a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) |
 
+visible: Default is **[form, content]**
+
 ### Dropdown
 
 You can use a `dropdown` element to add a dropdown menu in your form.
@@ -258,6 +276,8 @@ Validations:
 |----------|------------------------------------------------------|----------|---------|---------|--------------|
 | required | Prevents form submission until element is completed. | Optional | Boolean | false   | -            |
 
+visible: Default is **[form, content]**
+
 ### Checkboxes
 
 You can use the `checkboxes` element to add a set of checkboxes to your form.
@@ -265,17 +285,20 @@ You can use the `checkboxes` element to add a set of checkboxes to your form.
 Attributes:
 
 | Key         | Description                                                                                           | Required | Type   | Default      | Valid values |
-|-------------|-------------------------------------------------------------------------------------------------------|----------|--------|--------------|--------------|
+| ----------- | ----------------------------------------------------------------------------------------------------- | -------- | ------ | ------------ | ------------ |
 | label       | A brief description of the expected user input, which is displayed in the form.                       | Required | String | -            | -            |
 | description | A description of the set of checkboxes, which is displayed in the form. Supports Markdown formatting. | Optional | String | Empty String | -            |
 | options     | An array of checkboxes that the user can select. For syntax, see below.                               | Required | Array  | -            | -            |
 
 For each value in the options array, you can set the following keys.
 
-| Key      | Description                                                                                                                              | Required | Type    | Default | Options |
-|----------|------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|---------|---------|
-| label    | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String  | -       | -       |
-| required | Prevents form submission until element is completed.                                                                                     | Optional | Boolean | false   | -       |
+| Key          | Description                                                                                                                              | Required | Type         | Default | Options |
+|--------------|------------------------------------------------------------------------------------------------------------------------------------------|----------|--------------|---------|---------|
+| label        | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String       | -       | -       |
+| required     | Prevents form submission until element is completed.                                                                                     | Optional | Boolean      | false   | -       |
+| visible      | Whether a specific checkbox appears in the form only, in the created issue only, or both. Valid options are "form" and "content".        | Optional | String array | false   | -       |
+
+visible: Default is **[form, content]**
 
 ## Syntax for issue config
 
@@ -291,15 +314,15 @@ contact_links:
 
 ### Possible Options
 
-| Key                  | Description                                                                                           | Type               | Default        |
-|----------------------|-------------------------------------------------------------------------------------------------------|--------------------|----------------|
-| blank_issues_enabled | If set to false, the User is forced to use a Template                                                 | Boolean            | true           |
-| contact_links        | Custom Links to show in the Choose Box                                                                | Contact Link Array | Empty Array    |
+| Key                  | Description                                           | Type               | Default     |
+|----------------------|-------------------------------------------------------|--------------------|-------------|
+| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean            | true        |
+| contact_links        | Custom Links to show in the Choose Box                | Contact Link Array | Empty Array |
 
 ### Contact Link
 
-| Key                  | Description                                                                                           | Type    | Required |
-|----------------------|-------------------------------------------------------------------------------------------------------|---------|----------|
-| name  | the name of your link                                                                                                | String  | true     |
-| url   | The URL of your Link                                                                                                 | String  | true     |
-| about | A short description of your Link                                                                                     | String  | true     |
+| Key   | Description                      | Type   | Required |
+|-------|----------------------------------|--------|----------|
+| name  | the name of your link            | String | true     |
+| url   | The URL of your Link             | String | true     |
+| about | A short description of your Link | String | true     |
diff --git a/docs/content/usage/packages/container.en-us.md b/docs/content/usage/packages/container.en-us.md
index 6be21c2b27..5676aa36fb 100644
--- a/docs/content/usage/packages/container.en-us.md
+++ b/docs/content/usage/packages/container.en-us.md
@@ -39,6 +39,16 @@ Images must follow this naming convention:
 
 `{registry}/{owner}/{image}`
 
+When building your docker image, using the naming convention above, this looks like:
+
+```shell
+# build an image with tag
+docker build -t {registry}/{owner}/{image}:{tag} .
+# name an existing image with tag
+docker tag {some-existing-image}:{tag} {registry}/{owner}/{image}:{tag}
+```
+
+where your registry is the domain of your gitea instance (e.g. gitea.example.com).
 For example, these are all valid image names for the owner `testuser`:
 
 `gitea.example.com/testuser/myimage`
diff --git a/docs/content/usage/packages/overview.en-us.md b/docs/content/usage/packages/overview.en-us.md
index 44d18ff482..89fc6f286e 100644
--- a/docs/content/usage/packages/overview.en-us.md
+++ b/docs/content/usage/packages/overview.en-us.md
@@ -42,7 +42,7 @@ The following package managers are currently supported:
 | [PyPI](usage/packages/pypi.md) | Python | `pip`, `twine` |
 | [RPM](usage/packages/rpm.md) | - | `yum`, `dnf`, `zypper` |
 | [RubyGems](usage/packages/rubygems.md) | Ruby | `gem`, `Bundler` |
-| [Swift](usage/packages/rubygems.md) | Swift | `swift` |
+| [Swift](usage/packages/swift.md) | Swift | `swift` |
 | [Vagrant](usage/packages/vagrant.md) | - | `vagrant` |
 
 **The following paragraphs only apply if Packages are not globally disabled!**
diff --git a/docs/content/usage/packages/swift.en-us.md b/docs/content/usage/packages/swift.en-us.md
index 606fa20b36..38eb155641 100644
--- a/docs/content/usage/packages/swift.en-us.md
+++ b/docs/content/usage/packages/swift.en-us.md
@@ -26,7 +26,8 @@ To work with the Swift package registry, you need to use [swift](https://www.swi
 To register the package registry and provide credentials, execute:
 
 ```shell
-swift package-registry set https://gitea.example.com/api/packages/{owner}/swift -login {username} -password {password}
+swift package-registry set https://gitea.example.com/api/packages/{owner}/swift
+swift package-registry login https://gitea.example.com/api/packages/{owner}/swift --username {username} --password {password}
 ```
 
 | Placeholder  | Description |
diff --git a/docs/content/usage/profile-readme.zh-cn.md b/docs/content/usage/profile-readme.zh-cn.md
index 804f69d2e6..b69d4aa921 100644
--- a/docs/content/usage/profile-readme.zh-cn.md
+++ b/docs/content/usage/profile-readme.zh-cn.md
@@ -15,6 +15,10 @@ menu:
 
 # 个人资料 README
 
-要在您的 Gitea 个人资料页面显示一个 Markdown 文件,只需创建一个名为 ".profile" 的仓库,并编辑其中的 README.md 文件。Gitea 将自动获取该文件并在您的仓库上方显示。
+要在您的 Gitea 个人资料页面显示一个 Markdown 文件,只需创建一个名为 `.profile` 的仓库,并编辑其中的 `README.md` 文件。Gitea 将自动获取该文件并在您的仓库上方显示。
 
 注意:您可以将此仓库设为私有。这样可以隐藏您的源文件,使其对公众不可见,并允许您将某些文件设为私有。但是,README.md 文件将是您个人资料上唯一存在的文件。如果您希望完全私有化 .profile 仓库,则需删除或重命名 README.md 文件。
+
+用户示例 `.profile/README.md`:
+
+![个人资料自述文件截图](/images/usage/profile-readme.png)
diff --git a/docs/scripts/trans-copy.sh b/docs/scripts/trans-copy.sh
deleted file mode 100755
index 7374ab9e73..0000000000
--- a/docs/scripts/trans-copy.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-set -e
-
-#
-# This script is used to copy the en-US content to our available locales as a
-# fallback to always show all pages when displaying a specific locale that is
-# missing some documents to be translated.
-#
-# Just execute the script without any argument and you will get the missing
-# files copied into the content folder. We are calling this script within the CI
-# server simply by `make trans-copy`.
-#
-
-declare -a LOCALES=(
-  "fr-fr"
-  "nl-nl"
-  "pt-br"
-  "zh-cn"
-  "zh-tw"
-)
-
-ROOT=$(realpath $(dirname $0)/..)
-
-for SOURCE in $(find ${ROOT}/content -type f -iname *.en-us.md); do
-  for LOCALE in "${LOCALES[@]}"; do
-    DEST="${SOURCE%.en-us.md}.${LOCALE}.md"
-
-    if [[ ! -f ${DEST} ]]; then
-      cp ${SOURCE} ${DEST}
-      sed -i.bak "s/en\-us/${LOCALE}/g" ${DEST}
-      rm ${DEST}.bak
-    fi
-  done
-done
diff --git a/go.mod b/go.mod
index 7a752ec874..1e0f1ea8f8 100644
--- a/go.mod
+++ b/go.mod
@@ -1,143 +1,143 @@
 module code.gitea.io/gitea
 
-go 1.21
+go 1.22
 
 require (
-	code.gitea.io/actions-proto-go v0.3.1
+	code.gitea.io/actions-proto-go v0.4.0
 	code.gitea.io/gitea-vet v0.2.3
 	code.gitea.io/sdk/gitea v0.17.1
 	codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
-	gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669
+	connectrpc.com/connect v1.15.0
+	gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028
 	gitea.com/go-chi/cache v0.2.0
-	gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384
-	gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3
+	gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
+	gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96
 	gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
 	gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
 	github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
 	github.com/NYTimes/gziphandler v1.1.1
-	github.com/PuerkitoBio/goquery v1.8.1
-	github.com/alecthomas/chroma/v2 v2.12.0
+	github.com/PuerkitoBio/goquery v1.9.1
+	github.com/alecthomas/chroma/v2 v2.13.0
 	github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
 	github.com/blevesearch/bleve/v2 v2.3.10
-	github.com/bufbuild/connect-go v1.10.0
-	github.com/buildkite/terminal-to-html/v3 v3.10.1
+	github.com/buildkite/terminal-to-html/v3 v3.11.0
 	github.com/caddyserver/certmagic v0.20.0
 	github.com/chi-middleware/proxy v1.1.1
-	github.com/denisenkom/go-mssqldb v0.12.3
 	github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
 	github.com/djherbis/buffer v1.2.0
 	github.com/djherbis/nio/v3 v3.0.1
 	github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5
 	github.com/dustin/go-humanize v1.0.1
-	github.com/editorconfig/editorconfig-core-go/v2 v2.6.0
+	github.com/editorconfig/editorconfig-core-go/v2 v2.6.1
 	github.com/emersion/go-imap v1.2.1
 	github.com/emirpasic/gods v1.18.1
 	github.com/ethantkoenig/rupture v1.0.1
-	github.com/felixge/fgprof v0.9.3
+	github.com/felixge/fgprof v0.9.4
 	github.com/fsnotify/fsnotify v1.7.0
 	github.com/gliderlabs/ssh v0.3.6
-	github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9
+	github.com/go-ap/activitypub v0.0.0-20240316125321-b61fd6a83225
 	github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
-	github.com/go-chi/chi/v5 v5.0.11
+	github.com/go-chi/chi/v5 v5.0.12
 	github.com/go-chi/cors v1.2.1
 	github.com/go-co-op/gocron v1.37.0
-	github.com/go-enry/go-enry/v2 v2.8.6
+	github.com/go-enry/go-enry/v2 v2.8.7
 	github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e
 	github.com/go-git/go-billy/v5 v5.5.0
 	github.com/go-git/go-git/v5 v5.11.0
 	github.com/go-ldap/ldap/v3 v3.4.6
-	github.com/go-sql-driver/mysql v1.7.1
+	github.com/go-sql-driver/mysql v1.8.0
 	github.com/go-swagger/go-swagger v0.30.5
-	github.com/go-testfixtures/testfixtures/v3 v3.9.0
-	github.com/go-webauthn/webauthn v0.10.0
+	github.com/go-testfixtures/testfixtures/v3 v3.10.0
+	github.com/go-webauthn/webauthn v0.10.2
 	github.com/gobwas/glob v0.2.3
 	github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
 	github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
-	github.com/golang-jwt/jwt/v5 v5.2.0
+	github.com/golang-jwt/jwt/v5 v5.2.1
 	github.com/google/go-github/v57 v57.0.0
-	github.com/google/pprof v0.0.0-20240117000934-35fc243c5815
-	github.com/google/uuid v1.5.0
+	github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7
+	github.com/google/uuid v1.6.0
 	github.com/gorilla/feeds v1.1.2
 	github.com/gorilla/sessions v1.2.2
 	github.com/hashicorp/go-version v1.6.0
 	github.com/hashicorp/golang-lru/v2 v2.0.7
 	github.com/huandu/xstrings v1.4.0
 	github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
-	github.com/jhillyerd/enmime v1.1.0
+	github.com/jhillyerd/enmime v1.2.0
 	github.com/json-iterator/go v1.1.12
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
 	github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4
-	github.com/klauspost/compress v1.17.4
-	github.com/klauspost/cpuid/v2 v2.2.6
+	github.com/klauspost/compress v1.17.7
+	github.com/klauspost/cpuid/v2 v2.2.7
 	github.com/lib/pq v1.10.9
-	github.com/markbates/goth v1.78.0
+	github.com/markbates/goth v1.79.0
 	github.com/mattn/go-isatty v0.0.20
-	github.com/mattn/go-sqlite3 v1.14.19
-	github.com/meilisearch/meilisearch-go v0.26.1
+	github.com/mattn/go-sqlite3 v1.14.22
+	github.com/meilisearch/meilisearch-go v0.26.2
 	github.com/mholt/archiver/v3 v3.5.1
 	github.com/microcosm-cc/bluemonday v1.0.26
-	github.com/minio/minio-go/v7 v7.0.66
-	github.com/minio/sha256-simd v1.0.1
+	github.com/microsoft/go-mssqldb v1.7.0
+	github.com/minio/minio-go/v7 v7.0.69
 	github.com/msteinert/pam v1.2.0
 	github.com/nektos/act v0.2.52
 	github.com/niklasfasching/go-org v1.7.0
 	github.com/olivere/elastic/v7 v7.0.32
 	github.com/opencontainers/go-digest v1.0.0
-	github.com/opencontainers/image-spec v1.1.0-rc6
+	github.com/opencontainers/image-spec v1.1.0
 	github.com/pkg/errors v0.9.1
 	github.com/pquerna/otp v1.4.0
-	github.com/prometheus/client_golang v1.18.0
+	github.com/prometheus/client_golang v1.19.0
 	github.com/quasoft/websspi v1.1.2
-	github.com/redis/go-redis/v9 v9.4.0
+	github.com/redis/go-redis/v9 v9.5.1
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
-	github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd
+	github.com/sassoftware/go-rpmutils v0.3.0
 	github.com/sergi/go-diff v1.3.1
 	github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92
-	github.com/stretchr/testify v1.8.4
+	github.com/stretchr/testify v1.9.0
 	github.com/syndtr/goleveldb v1.0.0
 	github.com/tstranex/u2f v1.0.0
 	github.com/ulikunitz/xz v0.5.11
 	github.com/urfave/cli/v2 v2.27.1
-	github.com/xanzy/go-gitlab v0.96.0
+	github.com/xanzy/go-gitlab v0.100.0
 	github.com/xeipuuv/gojsonschema v1.2.0
 	github.com/yohcop/openid-go v1.0.1
-	github.com/yuin/goldmark v1.6.0
+	github.com/yuin/goldmark v1.7.0
 	github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
 	github.com/yuin/goldmark-meta v1.1.0
-	golang.org/x/crypto v0.18.0
+	golang.org/x/crypto v0.22.0
 	golang.org/x/image v0.15.0
-	golang.org/x/net v0.20.0
-	golang.org/x/oauth2 v0.16.0
-	golang.org/x/sys v0.16.0
+	golang.org/x/net v0.24.0
+	golang.org/x/oauth2 v0.18.0
+	golang.org/x/sys v0.19.0
 	golang.org/x/text v0.14.0
-	golang.org/x/tools v0.17.0
-	google.golang.org/grpc v1.60.1
-	google.golang.org/protobuf v1.32.0
+	golang.org/x/tools v0.19.0
+	google.golang.org/grpc v1.62.1
+	google.golang.org/protobuf v1.33.0
 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
 	gopkg.in/ini.v1 v1.67.0
 	gopkg.in/yaml.v3 v3.0.1
 	mvdan.cc/xurls/v2 v2.5.0
 	strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
 	xorm.io/builder v0.3.13
-	xorm.io/xorm v1.3.7
+	xorm.io/xorm v1.3.8
 )
 
 require (
-	cloud.google.com/go/compute v1.23.3 // indirect
+	cloud.google.com/go/compute v1.25.1 // indirect
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
 	dario.cat/mergo v1.0.0 // indirect
+	filippo.io/edwards25519 v1.1.0 // indirect
 	git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
-	github.com/ClickHouse/ch-go v0.61.1 // indirect
-	github.com/ClickHouse/clickhouse-go/v2 v2.17.1 // indirect
+	github.com/ClickHouse/ch-go v0.61.5 // indirect
+	github.com/ClickHouse/clickhouse-go/v2 v2.22.0 // indirect
 	github.com/DataDog/zstd v1.5.5 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.2.1 // indirect
 	github.com/Masterminds/sprig/v3 v3.2.3 // indirect
 	github.com/Microsoft/go-winio v0.6.1 // indirect
 	github.com/ProtonMail/go-crypto v1.0.0 // indirect
-	github.com/RoaringBitmap/roaring v1.7.0 // indirect
+	github.com/RoaringBitmap/roaring v1.9.0 // indirect
 	github.com/andybalholm/brotli v1.1.0 // indirect
 	github.com/andybalholm/cascadia v1.3.2 // indirect
 	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
@@ -145,12 +145,12 @@ require (
 	github.com/aymerick/douceur v0.2.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bits-and-blooms/bitset v1.13.0 // indirect
-	github.com/blevesearch/bleve_index_api v1.1.5 // indirect
-	github.com/blevesearch/geo v0.1.19 // indirect
+	github.com/blevesearch/bleve_index_api v1.1.6 // indirect
+	github.com/blevesearch/geo v0.1.20 // indirect
 	github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
 	github.com/blevesearch/gtreap v0.1.1 // indirect
 	github.com/blevesearch/mmap-go v1.0.4 // indirect
-	github.com/blevesearch/scorch_segment_api/v2 v2.2.6 // indirect
+	github.com/blevesearch/scorch_segment_api/v2 v2.2.8 // indirect
 	github.com/blevesearch/segment v0.9.1 // indirect
 	github.com/blevesearch/snowballstem v0.9.0 // indirect
 	github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
@@ -166,43 +166,43 @@ require (
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/cloudflare/circl v1.3.7 // indirect
 	github.com/couchbase/go-couchbase v0.1.1 // indirect
-	github.com/couchbase/gomemcached v0.3.0 // indirect
+	github.com/couchbase/gomemcached v0.3.1 // indirect
 	github.com/couchbase/goutils v0.1.2 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
 	github.com/cyphar/filepath-securejoin v0.2.4 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/davidmz/go-pageant v1.0.2 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
-	github.com/dlclark/regexp2 v1.10.0 // indirect
+	github.com/dlclark/regexp2 v1.11.0 // indirect
 	github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
 	github.com/fatih/color v1.16.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
-	github.com/fxamacker/cbor/v2 v2.5.0 // indirect
-	github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect
+	github.com/fxamacker/cbor/v2 v2.6.0 // indirect
+	github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 // indirect
 	github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
 	github.com/go-enry/go-oniguruma v1.2.1 // indirect
 	github.com/go-faster/city v1.0.1 // indirect
 	github.com/go-faster/errors v0.7.1 // indirect
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
-	github.com/go-openapi/analysis v0.22.2 // indirect
-	github.com/go-openapi/errors v0.21.0 // indirect
-	github.com/go-openapi/inflect v0.19.0 // indirect
-	github.com/go-openapi/jsonpointer v0.20.2 // indirect
-	github.com/go-openapi/jsonreference v0.20.4 // indirect
-	github.com/go-openapi/loads v0.21.5 // indirect
-	github.com/go-openapi/runtime v0.26.2 // indirect
-	github.com/go-openapi/spec v0.20.14 // indirect
-	github.com/go-openapi/strfmt v0.22.0 // indirect
-	github.com/go-openapi/swag v0.22.7 // indirect
-	github.com/go-openapi/validate v0.22.6 // indirect
-	github.com/go-webauthn/x v0.1.6 // indirect
+	github.com/go-openapi/analysis v0.23.0 // indirect
+	github.com/go-openapi/errors v0.22.0 // indirect
+	github.com/go-openapi/inflect v0.21.0 // indirect
+	github.com/go-openapi/jsonpointer v0.21.0 // indirect
+	github.com/go-openapi/jsonreference v0.21.0 // indirect
+	github.com/go-openapi/loads v0.22.0 // indirect
+	github.com/go-openapi/runtime v0.28.0 // indirect
+	github.com/go-openapi/spec v0.21.0 // indirect
+	github.com/go-openapi/strfmt v0.23.0 // indirect
+	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/go-openapi/validate v0.24.0 // indirect
+	github.com/go-webauthn/x v0.1.9 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
 	github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
 	github.com/golang-sql/sqlexp v0.1.0 // indirect
 	github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
-	github.com/golang/protobuf v1.5.3 // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/go-tpm v0.9.0 // indirect
@@ -230,6 +230,7 @@ require (
 	github.com/mholt/acmez v1.2.0 // indirect
 	github.com/miekg/dns v1.1.58 // indirect
 	github.com/minio/md5-simd v1.1.2 // indirect
+	github.com/minio/sha256-simd v1.0.1 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -241,16 +242,16 @@ require (
 	github.com/oklog/ulid v1.3.1 // indirect
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
 	github.com/onsi/ginkgo v1.16.5 // indirect
-	github.com/paulmach/orb v0.11.0 // indirect
+	github.com/paulmach/orb v0.11.1 // indirect
 	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
 	github.com/pierrec/lz4/v4 v4.1.21 // indirect
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	github.com/prometheus/client_model v0.5.0 // indirect
-	github.com/prometheus/common v0.46.0 // indirect
-	github.com/prometheus/procfs v0.12.0 // indirect
-	github.com/rhysd/actionlint v1.6.26 // indirect
-	github.com/rivo/uniseg v0.4.4 // indirect
+	github.com/prometheus/client_model v0.6.0 // indirect
+	github.com/prometheus/common v0.50.0 // indirect
+	github.com/prometheus/procfs v0.13.0 // indirect
+	github.com/rhysd/actionlint v1.6.27 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/rogpeppe/go-internal v1.12.0 // indirect
 	github.com/rs/xid v1.5.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -260,7 +261,7 @@ require (
 	github.com/shopspring/decimal v1.3.1 // indirect
 	github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
-	github.com/skeema/knownhosts v1.2.1 // indirect
+	github.com/skeema/knownhosts v1.2.2 // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
 	github.com/spf13/afero v1.11.0 // indirect
 	github.com/spf13/cast v1.6.0 // indirect
@@ -271,28 +272,28 @@ require (
 	github.com/toqueteos/webbrowser v1.2.0 // indirect
 	github.com/unknwon/com v1.0.1 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	github.com/valyala/fasthttp v1.51.0 // indirect
+	github.com/valyala/fasthttp v1.52.0 // indirect
 	github.com/valyala/fastjson v1.6.4 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
-	github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
+	github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
 	github.com/zeebo/blake3 v0.2.3 // indirect
-	go.etcd.io/bbolt v1.3.8 // indirect
-	go.mongodb.org/mongo-driver v1.13.1 // indirect
-	go.opentelemetry.io/otel v1.22.0 // indirect
-	go.opentelemetry.io/otel/trace v1.22.0 // indirect
+	go.etcd.io/bbolt v1.3.9 // indirect
+	go.mongodb.org/mongo-driver v1.14.0 // indirect
+	go.opentelemetry.io/otel v1.24.0 // indirect
+	go.opentelemetry.io/otel/trace v1.24.0 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	go.uber.org/zap v1.26.0 // indirect
-	golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
-	golang.org/x/mod v0.14.0 // indirect
+	go.uber.org/zap v1.27.0 // indirect
+	golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect
+	golang.org/x/mod v0.16.0 // indirect
 	golang.org/x/sync v0.6.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
@@ -302,7 +303,7 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
 
 replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
 
-replace github.com/nektos/act => gitea.com/gitea/act v0.2.51
+replace github.com/nektos/act => gitea.com/gitea/act v0.259.1
 
 replace github.com/gorilla/feeds => github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5
 
diff --git a/go.sum b/go.sum
index b3b8ad8ce4..864bed6677 100644
--- a/go.sum
+++ b/go.sum
@@ -1,64 +1,33 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
-cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
+cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
+cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-code.gitea.io/actions-proto-go v0.3.1 h1:PMyiQtBKb8dNnpEO2R5rcZdXSis+UQZVo/SciMtR1aU=
-code.gitea.io/actions-proto-go v0.3.1/go.mod h1:00ys5QDo1iHN1tHNvvddAcy2W/g+425hQya1cCSvq9A=
+code.gitea.io/actions-proto-go v0.4.0 h1:OsPBPhodXuQnsspG1sQ4eRE1PeoZyofd7+i73zCwnsU=
+code.gitea.io/actions-proto-go v0.4.0/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
 code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
 code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
 code.gitea.io/sdk/gitea v0.17.1 h1:3jCPOG2ojbl8AcfaUCRYLT5MUcBMFwS0OSK2mA5Zok8=
 code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM=
 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY=
 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM=
+connectrpc.com/connect v1.15.0 h1:lFdeCbZrVVDydAqwr4xGV2y+ULn+0Z73s5JBj2LikWo=
+connectrpc.com/connect v1.15.0/go.mod h1:bQmjpDY8xItMnttnurVgOkHUBMRT9cpsNi2O4AjKhmA=
 dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
 dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
 git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
-gitea.com/gitea/act v0.2.51 h1:gXc/B4OlTciTTzAx9cmNyw04n2SDO7exPjAsR5Idu+c=
-gitea.com/gitea/act v0.2.51/go.mod h1:CoaX2053jqBlD6JMgu4d4UgFL/rp2I14Kt5mMqcs0Z0=
-gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 h1:RUBX+MK/TsDxpHmymaOaydfigEbbzqUnG1OTZU/HAeo=
-gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc=
-gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0=
+gitea.com/gitea/act v0.259.1 h1:8GG1o/xtUHl3qjn5f0h/2FXrT5ubBn05TJOM5ry+FBw=
+gitea.com/gitea/act v0.259.1/go.mod h1:UxZWRYqQG2Yj4+4OqfGWW5a3HELwejyWFQyU7F1jUD8=
+gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028 h1:6/QAx4+s0dyRwdaTFPTnhGppuiuu0OqxIH9szyTpvKw=
+gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
 gitea.com/go-chi/cache v0.2.0 h1:E0npuTfDW6CT1yD8NMDVc1SK6IeRjfmRL2zlEsCEd7w=
 gitea.com/go-chi/cache v0.2.0/go.mod h1:iQlVK2aKTZ/rE9UcHyz9pQWGvdP9i1eI2spOpzgCrtE=
-gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384 h1:klh0LjhH7l4CuJkxlCM//o3rWLvWqxUpFxEtoYg5TNY=
-gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384/go.mod h1:hQ9SYHKdOX968wJglb/NMQ+UqpOKwW4L+EYdvkWjHSo=
-gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3 h1:4FuO+MahrkDjdjVIS8ExmY9FEHTZS8TPheEm4uU5xLI=
-gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3/go.mod h1:fc/pjt5EqNKgqQXYzcas1Z5L5whkZHyOvTA7OzWVJck=
+gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
+gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
+gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96 h1:IFDiMBObsP6CZIRaDLd54SR6zPYAffPXiXck5Xslu0Q=
+gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96/go.mod h1:0iEpFKnwO5dG0aF98O4eq6FMsAiXkNBaDIlUOlq4BtM=
 gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
 gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
@@ -69,18 +38,25 @@ github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 h1:r3qt8PCHnfjOv9PN3H
 github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121/go.mod h1:Ock8XgA7pvULhIaHGAk/cDnRfNrF9Jey81nPcc403iU=
 github.com/6543/go-version v1.3.1 h1:HvOp+Telns7HWJ2Xo/05YXQSB2bE0WmVgbHqwMPZT4U=
 github.com/6543/go-version v1.3.1/go.mod h1:oqFAHCwtLVUTLdhQmVZWYvaHXTdsbB4SY85at64SQEo=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
-github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/ClickHouse/ch-go v0.61.1 h1:j5rx3qnvcnYjhnP1IdXE/vdIRQiqgwAzyqOaasA6QCw=
-github.com/ClickHouse/ch-go v0.61.1/go.mod h1:myxt/JZgy2BYHFGQqzmaIpbfr5CMbs3YHVULaWQj5YU=
-github.com/ClickHouse/clickhouse-go/v2 v2.17.1 h1:ZCmAYWpu75IyEi7+Yrs/uaAjiCGY5wfW5kXo64exkX4=
-github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ=
-github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
+github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4=
+github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg=
+github.com/ClickHouse/clickhouse-go/v2 v2.22.0 h1:LAdk0qT125PpSPnYepFQs5X5z1EwpAtIX10SUELPgi0=
+github.com/ClickHouse/clickhouse-go/v2 v2.22.0/go.mod h1:tBhdF3f3RdP7sS59+oBAtTyhWpy0024ZxDMhgxra0QE=
 github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
 github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
 github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
@@ -98,27 +74,26 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cq
 github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
 github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
 github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
-github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
-github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
+github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
+github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
 github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
 github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I=
-github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
-github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
-github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
-github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
+github.com/RoaringBitmap/roaring v1.9.0 h1:lwKhr90/j0jVXJyh5X+vQN1VVn77rQFfYnh6RDRGCcE=
+github.com/RoaringBitmap/roaring v1.9.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
+github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
+github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
 github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
-github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
-github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
+github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
+github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
 github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
-github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
-github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
 github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
 github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
-github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
 github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
 github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -143,10 +118,10 @@ github.com/blevesearch/bleve/v2 v2.0.5/go.mod h1:ZjWibgnbRX33c+vBRgla9QhPb4QOjD6
 github.com/blevesearch/bleve/v2 v2.3.10 h1:z8V0wwGoL4rp7nG/O3qVVLYxUqCbEwskMt4iRJsPLgg=
 github.com/blevesearch/bleve/v2 v2.3.10/go.mod h1:RJzeoeHC+vNHsoLR54+crS1HmOWpnH87fL70HAUCzIA=
 github.com/blevesearch/bleve_index_api v1.0.0/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
-github.com/blevesearch/bleve_index_api v1.1.5 h1:0q05mzu6GT/kebzqKywCpou/eUea9wTKa7kfqX7QX+k=
-github.com/blevesearch/bleve_index_api v1.1.5/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
-github.com/blevesearch/geo v0.1.19 h1:hlX1YpBZ+X+xfjS8hEpmM/tdPUFbqBME3mdAWKHo2s0=
-github.com/blevesearch/geo v0.1.19/go.mod h1:EPyr3iJCcESYa830PnkFhqzJkOP7/daHT/ocun43WRY=
+github.com/blevesearch/bleve_index_api v1.1.6 h1:orkqDFCBuNU2oHW9hN2YEJmet+TE9orml3FCGbl1cKk=
+github.com/blevesearch/bleve_index_api v1.1.6/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
+github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
+github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
 github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
 github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
 github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
@@ -155,8 +130,8 @@ github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+
 github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
 github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
 github.com/blevesearch/scorch_segment_api/v2 v2.0.1/go.mod h1:lq7yK2jQy1yQjtjTfU931aVqz7pYxEudHaDwOt1tXfU=
-github.com/blevesearch/scorch_segment_api/v2 v2.2.6 h1:rewrzgFaCEjjfWovAB9NubMAd4+aCLxD3RaQcPDaoNo=
-github.com/blevesearch/scorch_segment_api/v2 v2.2.6/go.mod h1:0rv+k/OIjtYCT/g7Z45pCOVweFyta+0AdXO8keKfZxo=
+github.com/blevesearch/scorch_segment_api/v2 v2.2.8 h1:+OLW38LuRKio6N6V0gIk1srwFz79FJ5v2sNqHz2HVAA=
+github.com/blevesearch/scorch_segment_api/v2 v2.2.8/go.mod h1:ckbeb7knyOOvAdZinn/ASbB7EA3HoagnJkmEV3J7+sg=
 github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
 github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
 github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
@@ -194,40 +169,34 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
 github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
 github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
 github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
-github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg=
-github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8=
-github.com/buildkite/terminal-to-html/v3 v3.10.1 h1:znT9eD26LQ59dDJJEpMCwkP4wEptEAPi74hsTBuHdEo=
-github.com/buildkite/terminal-to-html/v3 v3.10.1/go.mod h1:qtuRyYs6/Sw3FS9jUyVEaANHgHGqZsGqMknPLyau5cQ=
+github.com/buildkite/terminal-to-html/v3 v3.11.0 h1:wMTpKgR61lqmxMz1FKjCaW5mq6DqeEgFZdJ+SU4hP30=
+github.com/buildkite/terminal-to-html/v3 v3.11.0/go.mod h1:8JACDet3vmvWLsL4IBobweQYtf19W5J+EKM3LEE1c+4=
 github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
 github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
 github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ=
 github.com/chi-middleware/proxy v1.1.1/go.mod h1:jQwMEJct2tz9VmtCELxvnXoMfa+SOdikvbVJVHv/M+0=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
+github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
+github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
+github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
+github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
+github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
 github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
 github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
 github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k=
-github.com/couchbase/go-couchbase v0.0.0-20201026062457-7b3be89bbd89/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A=
 github.com/couchbase/go-couchbase v0.1.1 h1:ClFXELcKj/ojyoTYbsY34QUrrYCBi/1G749sXSCkdhk=
 github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A=
-github.com/couchbase/gomemcached v0.1.1/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo=
-github.com/couchbase/gomemcached v0.3.0 h1:XkMDdP6w7rtvLijDE0/RhcccX+XvAk5cboyBv1YcI0U=
-github.com/couchbase/gomemcached v0.3.0/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo=
-github.com/couchbase/goutils v0.0.0-20201030094643-5e82bb967e67/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
+github.com/couchbase/gomemcached v0.3.1 h1:jfspNuQIXgWy+5GUPQrsQ6yC5uJCfMmd/JKvK6C26r8=
+github.com/couchbase/gomemcached v0.3.1/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo=
 github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9BCs=
 github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE=
 github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
@@ -244,8 +213,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
 github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
-github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
 github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
 github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
@@ -260,17 +227,16 @@ github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmW
 github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
-github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/dvyukov/go-fuzz v0.0.0-20210429054444-fca39067bc72/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
-github.com/editorconfig/editorconfig-core-go/v2 v2.6.0 h1:5O8paxMLmi/5ONoKXzWNYxoSZU7+ITVbGcPga0IrzfE=
-github.com/editorconfig/editorconfig-core-go/v2 v2.6.0/go.mod h1:hdTKe+hwa3mMnMn4JUQziT+yc3pF+6EVmK2LPbLZthE=
+github.com/editorconfig/editorconfig-core-go/v2 v2.6.1 h1:iPCqofzMO41WVbcS/B5Ym7AwHQg9cyQ7Ie/R2XU5L3A=
+github.com/editorconfig/editorconfig-core-go/v2 v2.6.1/go.mod h1:VY4oyqUnpULFB3SCRpl24GFDIN1PmfiQIvN/G4ScSNg=
 github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
 github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
 github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
@@ -284,16 +250,12 @@ github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTe
 github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/ethantkoenig/rupture v1.0.1 h1:6aAXghmvtnngMgQzy7SMGdicMvkV86V4n9fT0meE5E4=
 github.com/ethantkoenig/rupture v1.0.1/go.mod h1:Sjqo/nbffZp1pVVXNGhpugIjsWmuS9KiIB4GtpEBur4=
 github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
-github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
-github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
+github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88=
+github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
@@ -304,30 +266,29 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
-github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
+github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
+github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
 github.com/gliderlabs/ssh v0.3.6 h1:ZzjlDa05TcFRICb3anf/dSPN3ewz1Zx6CMLPWgkm3b8=
 github.com/gliderlabs/ssh v0.3.6/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
 github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
 github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
-github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 h1:j2TrkUG/NATGi/EQS+MvEoF79CxiRUmT16ErFroNcKI=
-github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9/go.mod h1:cJ9Ye0ZNSMN7RzZDBRY3E+8M3Bpf/R1JX22Ir9yX6WI=
-github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 h1:I2nuhyVI/48VXoRCCZR2hYBgnSXa+EuDJf/VyX06TC0=
-github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7/go.mod h1:5x8a6P/dhmMGFxWLcyYlyOuJ2lRNaHGhRv+yu8BaTSI=
+github.com/go-ap/activitypub v0.0.0-20240316125321-b61fd6a83225 h1:OoM81OclgRX7CUch4M7MmsH0NcmLWpFiSn7rhs6Y5ZU=
+github.com/go-ap/activitypub v0.0.0-20240316125321-b61fd6a83225/go.mod h1:yRUfFCoZY6C1CWalauqEQ5xYgSckzEBEO/2MBC6BOME=
+github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 h1:H9MGShwybHLSln6K8RxHPMHiLcD86Lru+5TVW2TcXHY=
+github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0/go.mod h1:5x8a6P/dhmMGFxWLcyYlyOuJ2lRNaHGhRv+yu8BaTSI=
 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw=
 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
 github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
 github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
-github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
-github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
-github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
+github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
 github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
 github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
 github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
-github.com/go-enry/go-enry/v2 v2.8.6 h1:T6ljs5+qNiUTDqpfK5GUD5EvLNdDbf804u8iC30vw7U=
-github.com/go-enry/go-enry/v2 v2.8.6/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
+github.com/go-enry/go-enry/v2 v2.8.7 h1:vbab0pcf5Yo1cHQLzbWZ+QomUh3EfEU8EiR5n7W0lnQ=
+github.com/go-enry/go-enry/v2 v2.8.7/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
 github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
 github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
 github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
@@ -345,38 +306,34 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
 github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
 github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A=
 github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc=
-github.com/go-openapi/analysis v0.22.2 h1:ZBmNoP2h5omLKr/srIC9bfqrUGzT6g6gNv03HE9Vpj0=
-github.com/go-openapi/analysis v0.22.2/go.mod h1:pDF4UbZsQTo/oNuRfAWWd4dAh4yuYf//LYorPTjrpvo=
-github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY=
-github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho=
-github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
-github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
-github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
-github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
-github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
-github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
-github.com/go-openapi/loads v0.21.5 h1:jDzF4dSoHw6ZFADCGltDb2lE4F6De7aWSpe+IcsRzT0=
-github.com/go-openapi/loads v0.21.5/go.mod h1:PxTsnFBoBe+z89riT+wYt3prmSBP6GDAQh2l9H1Flz8=
-github.com/go-openapi/runtime v0.26.2 h1:elWyB9MacRzvIVgAZCBJmqTi7hBzU0hlKD4IvfX0Zl0=
-github.com/go-openapi/runtime v0.26.2/go.mod h1:O034jyRZ557uJKzngbMDJXkcKJVzXJiymdSfgejrcRw=
-github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do=
-github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
-github.com/go-openapi/strfmt v0.22.0 h1:Ew9PnEYc246TwrEspvBdDHS4BVKXy/AOVsfqGDgAcaI=
-github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4=
-github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8=
-github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0=
-github.com/go-openapi/validate v0.22.6 h1:+NhuwcEYpWdO5Nm4bmvhGLW0rt1Fcc532Mu3wpypXfo=
-github.com/go-openapi/validate v0.22.6/go.mod h1:eaddXSqKeTg5XpSmj1dYyFTK/95n/XHwcOY+BMxKMyM=
+github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
+github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
+github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
+github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
+github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk=
+github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
+github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
+github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=
+github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
+github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
+github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
+github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
+github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
+github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
 github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
-github.com/go-redis/redis/v8 v8.4.0/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M=
 github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
-github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
-github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
+github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/go-swagger/go-swagger v0.30.5 h1:SQ2+xSonWjjoEMOV5tcOnZJVlfyUfCBhGQGArS1b9+U=
 github.com/go-swagger/go-swagger v0.30.5/go.mod h1:cWUhSyCNqV7J1wkkxfr5QmbcnCewetCdvEXqgPvbc/Q=
 github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 h1:l9rI6sNaZgNC0LnF3MiE+qTmyBA/tZAg1rtyrGbUMK0=
@@ -384,16 +341,17 @@ github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.m
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
 github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
-github.com/go-testfixtures/testfixtures/v3 v3.9.0 h1:938g5V+GWLVejm3Hc+nWCuEXRlcglZDDlN/t1gWzcSY=
-github.com/go-testfixtures/testfixtures/v3 v3.9.0/go.mod h1:cdsKD2ApFBjdog9jRsz6EJqF+LClq/hrwE9K/1Dzo4s=
-github.com/go-webauthn/webauthn v0.10.0 h1:yuW2e1tXnRAwAvKrR4q4LQmc6XtCMH639/ypZGhZCwk=
-github.com/go-webauthn/webauthn v0.10.0/go.mod h1:l0NiauXhL6usIKqNLCUM3Qir43GK7ORg8ggold0Uv/Y=
-github.com/go-webauthn/x v0.1.6 h1:QNAX+AWeqRt9loE8mULeWJCqhVG5D/jvdmJ47fIWCkQ=
-github.com/go-webauthn/x v0.1.6/go.mod h1:W8dFVZ79o4f+nY1eOUICy/uq5dhrRl7mxQkYhXTo0FA=
+github.com/go-testfixtures/testfixtures/v3 v3.10.0 h1:BrBwN7AuC+74g5qtk9D59TLGOaEa8Bw1WmIsf+SyzWc=
+github.com/go-testfixtures/testfixtures/v3 v3.10.0/go.mod h1:z8RoleoNtibi6Ar8ziCW7e6PQ+jWiqbUWvuv8AMe4lo=
+github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4=
+github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs=
+github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE=
+github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
-github.com/goccy/go-json v0.9.5/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@@ -401,64 +359,40 @@ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7w
 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0=
 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU=
-github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
-github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
-github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
 github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
 github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
 github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
 github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
 github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -471,27 +405,13 @@ github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
-github.com/google/pprof v0.0.0-20240117000934-35fc243c5815 h1:WzfWbQz/Ze8v6l++GGbGNFZnUShVpP/0xffCPLL+ax8=
-github.com/google/pprof v0.0.0-20240117000934-35fc243c5815/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
+github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
-github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw=
@@ -502,7 +422,6 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
 github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
 github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
 github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
-github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
 github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
 github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5oVvhMjDyLhmA1LG86oSo+IqY=
@@ -510,7 +429,6 @@ github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5
 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
 github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
 github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
-github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
 github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
 github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
 github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
@@ -521,8 +439,6 @@ github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+
 github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
 github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
 github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -533,8 +449,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
 github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
 github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
+github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
 github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
@@ -555,23 +470,20 @@ github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
 github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
 github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
 github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
-github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
 github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
-github.com/jhillyerd/enmime v1.1.0 h1:ubaIzg68VY7CMCe2YbHe6nkRvU9vujixTkNz3EBvZOw=
-github.com/jhillyerd/enmime v1.1.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
+github.com/jhillyerd/enmime v1.2.0 h1:dIu1IPEymQgoT2dzuB//ttA/xcV40NMPpQtmd4wslHk=
+github.com/jhillyerd/enmime v1.2.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
@@ -585,17 +497,16 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
-github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
-github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
+github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
-github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
-github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
 github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
 github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@@ -609,12 +520,9 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
-github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
-github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
-github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
-github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
-github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
@@ -629,11 +537,10 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
 github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE=
 github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o=
-github.com/markbates/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
-github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
+github.com/markbates/goth v1.79.0 h1:fUYi9R6VubVEK2bpmXvIUp7xRcxA68i8ovfUQx/i5Qc=
+github.com/markbates/goth v1.79.0/go.mod h1:RBD+tcFnXul2NnYuODhnIweOcuVPkBohLfEvutPekcU=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -643,22 +550,24 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
 github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
-github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
-github.com/meilisearch/meilisearch-go v0.26.1 h1:3bmo2uLijX7kvBmiZ9LupVfC95TFcRJDgrRTzbOoE4A=
-github.com/meilisearch/meilisearch-go v0.26.1/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/meilisearch/meilisearch-go v0.26.2 h1:3gTlmiV1dHHumVUhYdJbvh3camiNiyqQ1hNveVsU2OE=
+github.com/meilisearch/meilisearch-go v0.26.2/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
 github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
 github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
 github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
 github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
 github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
 github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
+github.com/microsoft/go-mssqldb v1.7.0 h1:sgMPW0HA6Ihd37Yx0MzHyKD726C2kY/8KJsQtXHNaAs=
+github.com/microsoft/go-mssqldb v1.7.0/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
 github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
 github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
-github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw=
-github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs=
+github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0=
+github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
 github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
@@ -676,9 +585,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
 github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
-github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
 github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
@@ -704,22 +611,21 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
 github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
 github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=
-github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
-github.com/paulmach/orb v0.11.0 h1:JfVXJUBeH9ifc/OrhBY0lL16QsmPgpCHMlqSSYhcgAA=
-github.com/paulmach/orb v0.11.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
+github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
+github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
+github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
+github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
 github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
@@ -731,7 +637,8 @@ github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ
 github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
 github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
-github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -740,31 +647,29 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
 github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
-github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
-github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
-github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
-github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
-github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
-github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
+github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
+github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
+github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
+github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
+github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
+github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
+github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
 github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw=
 github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk=
 github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
-github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
+github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
+github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
-github.com/rhysd/actionlint v1.6.26 h1:zi7jPZf3Ks14gCXYAAL47uBziyFlX7+Xwilqhexct9g=
-github.com/rhysd/actionlint v1.6.26/go.mod h1:TIj1DlCgtYLOv5CH9wCK+WJTOr1qAdnFzkGi0IgSCO4=
+github.com/rhysd/actionlint v1.6.27 h1:xxwe8YmveBcC8lydW6GoHMGmB6H/MTqUU60F2p10wjw=
+github.com/rhysd/actionlint v1.6.27/go.mod h1:m2nFUjAnOrxCMXuOMz9evYBRCLUsMnKY2IJl/N5umbk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
-github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
 github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
@@ -781,8 +686,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g
 github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
-github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd h1:KpbqRPDwcAQTyaP+L+YudTRb3CnJlQ64Hfn1SF/zHBA=
-github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd/go.mod h1:TJJQYtLe/BeEmEjelI3b7xNZjzAukEkeWKmoakvaOoI=
+github.com/sassoftware/go-rpmutils v0.3.0 h1:tE4TZ8KcOXay5iIP64P291s6Qxd9MQCYhI7DU+f3gFA=
+github.com/sassoftware/go-rpmutils v0.3.0/go.mod h1:hM9wdxFsjUFR/tJ6SMsLrJuChcucCa0DsCzE9RMfwMo=
 github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
 github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
 github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
@@ -800,8 +705,8 @@ github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
-github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
+github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
+github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v1.1.1 h1:T/YLemO5Yp7KPzS+lVtu+WsHn8yoSwTfItdAd1r3cck=
@@ -843,8 +748,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
@@ -868,23 +774,21 @@ github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6S
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
-github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
-github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
+github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
+github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
 github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
 github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
 github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
 github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
-github.com/xanzy/go-gitlab v0.96.0 h1:LGkZ+wSNMRtHIBaYE4Hq3dZVjprwHv3Y1+rhKU3WETs=
-github.com/xanzy/go-gitlab v0.96.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI=
+github.com/xanzy/go-gitlab v0.100.0 h1:jaOtYj5nWI19+9oVVmgy233pax2oYqucwetogYU46ks=
+github.com/xanzy/go-gitlab v0.100.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI=
 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
 github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
 github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
 github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
-github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
 github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
-github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -895,8 +799,8 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
-github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
-github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
+github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
 github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5 h1:3seWKGVhGoc66Ht5QlhQsr4xT2caDnFegsnh2NqvENU=
 github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
 github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=
@@ -904,12 +808,11 @@ github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBz
 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
-github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
+github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
 github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
@@ -921,39 +824,29 @@ github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvv
 github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
 github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
 go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
-go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
-go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
+go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
+go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
 go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
-go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
-go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw=
-go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
-go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
-go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
-go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
+go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
+go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
-go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
-go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
-go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
-go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -963,80 +856,31 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
-golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
-golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw=
+golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
 golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
 golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
-golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
+golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -1046,23 +890,13 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
-golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
+golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1070,43 +904,21 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190730183949-1393eb018365/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1114,9 +926,9 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/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-20211007075335-d3039528d8ac/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-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1129,9 +941,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -1141,11 +952,9 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
-golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
-golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -1158,158 +967,44 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
 golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
-golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
-golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
+golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
+golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
 google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
-google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
+google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
+google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
-google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1322,7 +1017,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
 gopkg.in/ini.v1 v1.44.2/go.mod h1:M3Cogqpuv0QCi3ExAY5V4uOt4qb/R3xZubo9m8lK5wg=
-gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@@ -1337,16 +1031,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
 lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
 modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
@@ -1369,12 +1055,9 @@ modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
 modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
 mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs=
 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
 xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
 xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
-xorm.io/xorm v1.3.7 h1:mLceAGu0b87r9pD4qXyxGHxifOXIIrAdVcA6k95/osw=
-xorm.io/xorm v1.3.7/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
+xorm.io/xorm v1.3.8 h1:CJmplmWqfSRpLWSPMmqz+so8toBp3m7ehuRehIWedZo=
+xorm.io/xorm v1.3.8/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
diff --git a/models/actions/artifact.go b/models/actions/artifact.go
index 5390f6288f..3d0a288e62 100644
--- a/models/actions/artifact.go
+++ b/models/actions/artifact.go
@@ -26,6 +26,8 @@ const (
 	ArtifactStatusUploadConfirmed                           // 2, ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed
 	ArtifactStatusUploadError                               // 3, ArtifactStatusUploadError is the status of an artifact upload that is errored
 	ArtifactStatusExpired                                   // 4, ArtifactStatusExpired is the status of an artifact that is expired
+	ArtifactStatusPendingDeletion                           // 5, ArtifactStatusPendingDeletion is the status of an artifact that is pending deletion
+	ArtifactStatusDeleted                                   // 6, ArtifactStatusDeleted is the status of an artifact that is deleted
 )
 
 func init() {
@@ -147,8 +149,28 @@ func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) {
 		Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts)
 }
 
+// ListPendingDeleteArtifacts returns all artifacts in pending-delete status.
+// limit is the max number of artifacts to return.
+func ListPendingDeleteArtifacts(ctx context.Context, limit int) ([]*ActionArtifact, error) {
+	arts := make([]*ActionArtifact, 0, limit)
+	return arts, db.GetEngine(ctx).
+		Where("status = ?", ArtifactStatusPendingDeletion).Limit(limit).Find(&arts)
+}
+
 // SetArtifactExpired sets an artifact to expired
 func SetArtifactExpired(ctx context.Context, artifactID int64) error {
 	_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)})
 	return err
 }
+
+// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it
+func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error {
+	_, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusPendingDeletion)})
+	return err
+}
+
+// SetArtifactDeleted sets an artifact to deleted
+func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
+	_, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusDeleted)})
+	return err
+}
diff --git a/models/actions/run.go b/models/actions/run.go
index fcac58d515..fa9db0b554 100644
--- a/models/actions/run.go
+++ b/models/actions/run.go
@@ -170,15 +170,16 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err
 	return err
 }
 
-// CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow.
-func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
-	// Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'.
+// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
+// It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
+func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
+	// Find all runs in the specified repository, reference, and workflow with non-final status
 	runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
 		RepoID:       repoID,
 		Ref:          ref,
 		WorkflowID:   workflowID,
 		TriggerEvent: event,
-		Status:       []Status{StatusRunning, StatusWaiting},
+		Status:       []Status{StatusRunning, StatusWaiting, StatusBlocked},
 	})
 	if err != nil {
 		return err
@@ -339,6 +340,23 @@ func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error)
 	return run, nil
 }
 
+func GetWorkflowLatestRun(ctx context.Context, repoID int64, workflowFile, branch, event string) (*ActionRun, error) {
+	var run ActionRun
+	q := db.GetEngine(ctx).Where("repo_id=?", repoID).
+		And("ref = ?", branch).
+		And("workflow_id = ?", workflowFile)
+	if event != "" {
+		q.And("event = ?", event)
+	}
+	has, err := q.Desc("id").Get(&run)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, util.NewNotExistErrorf("run with repo_id %d, ref %s, workflow_id %s", repoID, branch, workflowFile)
+	}
+	return &run, nil
+}
+
 // UpdateRun updates a run.
 // It requires the inputted run has Version set.
 // It will return error if the version is not matched (it means the run has been changed after loaded).
diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go
index 6ea6cb9d3b..6c5d3b3252 100644
--- a/models/actions/run_job_list.go
+++ b/models/actions/run_job_list.go
@@ -16,14 +16,9 @@ import (
 type ActionJobList []*ActionRunJob
 
 func (jobs ActionJobList) GetRunIDs() []int64 {
-	ids := make(container.Set[int64], len(jobs))
-	for _, j := range jobs {
-		if j.RunID == 0 {
-			continue
-		}
-		ids.Add(j.RunID)
-	}
-	return ids.Values()
+	return container.FilterSlice(jobs, func(j *ActionRunJob) (int64, bool) {
+		return j.RunID, j.RunID != 0
+	})
 }
 
 func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error {
diff --git a/models/actions/run_list.go b/models/actions/run_list.go
index 388bfc4f86..4046c7d369 100644
--- a/models/actions/run_list.go
+++ b/models/actions/run_list.go
@@ -19,19 +19,15 @@ type RunList []*ActionRun
 
 // GetUserIDs returns a slice of user's id
 func (runs RunList) GetUserIDs() []int64 {
-	ids := make(container.Set[int64], len(runs))
-	for _, run := range runs {
-		ids.Add(run.TriggerUserID)
-	}
-	return ids.Values()
+	return container.FilterSlice(runs, func(run *ActionRun) (int64, bool) {
+		return run.TriggerUserID, true
+	})
 }
 
 func (runs RunList) GetRepoIDs() []int64 {
-	ids := make(container.Set[int64], len(runs))
-	for _, run := range runs {
-		ids.Add(run.RepoID)
-	}
-	return ids.Values()
+	return container.FilterSlice(runs, func(run *ActionRun) (int64, bool) {
+		return run.RepoID, true
+	})
 }
 
 func (runs RunList) LoadTriggerUser(ctx context.Context) error {
diff --git a/models/actions/runner.go b/models/actions/runner.go
index 4103ba4477..67f003387b 100644
--- a/models/actions/runner.go
+++ b/models/actions/runner.go
@@ -13,6 +13,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/shared/types"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/modules/util"
@@ -97,7 +98,7 @@ func (r *ActionRunner) StatusName() string {
 }
 
 func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string {
-	return lang.Tr("actions.runners.status." + r.StatusName())
+	return lang.TrString("actions.runners.status." + r.StatusName())
 }
 
 func (r *ActionRunner) IsOnline() bool {
@@ -159,7 +160,7 @@ type FindRunnerOptions struct {
 	OwnerID       int64
 	Sort          string
 	Filter        string
-	IsOnline      util.OptionalBool
+	IsOnline      optional.Option[bool]
 	WithAvailable bool // not only runners belong to, but also runners can be used
 }
 
@@ -186,10 +187,12 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
 		cond = cond.And(builder.Like{"name", opts.Filter})
 	}
 
-	if opts.IsOnline.IsTrue() {
-		cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
-	} else if opts.IsOnline.IsFalse() {
-		cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
+	if opts.IsOnline.Has() {
+		if opts.IsOnline.Value() {
+			cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
+		} else {
+			cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
+		}
 	}
 	return cond
 }
diff --git a/models/actions/runner_list.go b/models/actions/runner_list.go
index 87f0886b47..3ef8ebb254 100644
--- a/models/actions/runner_list.go
+++ b/models/actions/runner_list.go
@@ -16,14 +16,9 @@ type RunnerList []*ActionRunner
 
 // GetUserIDs returns a slice of user's id
 func (runners RunnerList) GetUserIDs() []int64 {
-	ids := make(container.Set[int64], len(runners))
-	for _, runner := range runners {
-		if runner.OwnerID == 0 {
-			continue
-		}
-		ids.Add(runner.OwnerID)
-	}
-	return ids.Values()
+	return container.FilterSlice(runners, func(runner *ActionRunner) (int64, bool) {
+		return runner.OwnerID, runner.OwnerID != 0
+	})
 }
 
 func (runners RunnerList) LoadOwners(ctx context.Context) error {
@@ -41,16 +36,9 @@ func (runners RunnerList) LoadOwners(ctx context.Context) error {
 }
 
 func (runners RunnerList) getRepoIDs() []int64 {
-	repoIDs := make(container.Set[int64], len(runners))
-	for _, runner := range runners {
-		if runner.RepoID == 0 {
-			continue
-		}
-		if _, ok := repoIDs[runner.RepoID]; !ok {
-			repoIDs[runner.RepoID] = struct{}{}
-		}
-	}
-	return repoIDs.Values()
+	return container.FilterSlice(runners, func(runner *ActionRunner) (int64, bool) {
+		return runner.RepoID, runner.RepoID > 0
+	})
 }
 
 func (runners RunnerList) LoadRepos(ctx context.Context) error {
diff --git a/models/actions/schedule.go b/models/actions/schedule.go
index d450e7aa07..3646a046a0 100644
--- a/models/actions/schedule.go
+++ b/models/actions/schedule.go
@@ -127,14 +127,14 @@ func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) er
 		return fmt.Errorf("DeleteCronTaskByRepo: %v", err)
 	}
 	// cancel running cron jobs of this repository and delete old schedules
-	if err := CancelRunningJobs(
+	if err := CancelPreviousJobs(
 		ctx,
 		repo.ID,
 		repo.DefaultBranch,
 		"",
 		webhook_module.HookEventSchedule,
 	); err != nil {
-		return fmt.Errorf("CancelRunningJobs: %v", err)
+		return fmt.Errorf("CancelPreviousJobs: %v", err)
 	}
 	return nil
 }
diff --git a/models/actions/schedule_list.go b/models/actions/schedule_list.go
index b806550b87..5361b94801 100644
--- a/models/actions/schedule_list.go
+++ b/models/actions/schedule_list.go
@@ -18,19 +18,15 @@ type ScheduleList []*ActionSchedule
 
 // GetUserIDs returns a slice of user's id
 func (schedules ScheduleList) GetUserIDs() []int64 {
-	ids := make(container.Set[int64], len(schedules))
-	for _, schedule := range schedules {
-		ids.Add(schedule.TriggerUserID)
-	}
-	return ids.Values()
+	return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) {
+		return schedule.TriggerUserID, true
+	})
 }
 
 func (schedules ScheduleList) GetRepoIDs() []int64 {
-	ids := make(container.Set[int64], len(schedules))
-	for _, schedule := range schedules {
-		ids.Add(schedule.RepoID)
-	}
-	return ids.Values()
+	return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) {
+		return schedule.RepoID, true
+	})
 }
 
 func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error {
@@ -44,6 +40,9 @@ func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error {
 			schedule.TriggerUser = user_model.NewActionsUser()
 		} else {
 			schedule.TriggerUser = users[schedule.TriggerUserID]
+			if schedule.TriggerUser == nil {
+				schedule.TriggerUser = user_model.NewGhostUser()
+			}
 		}
 	}
 	return nil
diff --git a/models/actions/schedule_spec_list.go b/models/actions/schedule_spec_list.go
index e9ae268a6e..f7dac72f8b 100644
--- a/models/actions/schedule_spec_list.go
+++ b/models/actions/schedule_spec_list.go
@@ -16,11 +16,9 @@ import (
 type SpecList []*ActionScheduleSpec
 
 func (specs SpecList) GetScheduleIDs() []int64 {
-	ids := make(container.Set[int64], len(specs))
-	for _, spec := range specs {
-		ids.Add(spec.ScheduleID)
-	}
-	return ids.Values()
+	return container.FilterSlice(specs, func(spec *ActionScheduleSpec) (int64, bool) {
+		return spec.ScheduleID, true
+	})
 }
 
 func (specs SpecList) LoadSchedules(ctx context.Context) error {
@@ -46,11 +44,9 @@ func (specs SpecList) LoadSchedules(ctx context.Context) error {
 }
 
 func (specs SpecList) GetRepoIDs() []int64 {
-	ids := make(container.Set[int64], len(specs))
-	for _, spec := range specs {
-		ids.Add(spec.RepoID)
-	}
-	return ids.Values()
+	return container.FilterSlice(specs, func(spec *ActionScheduleSpec) (int64, bool) {
+		return spec.RepoID, true
+	})
 }
 
 func (specs SpecList) LoadRepos(ctx context.Context) error {
diff --git a/models/actions/status.go b/models/actions/status.go
index c97578f2ac..eda2234137 100644
--- a/models/actions/status.go
+++ b/models/actions/status.go
@@ -41,7 +41,7 @@ func (s Status) String() string {
 
 // LocaleString returns the locale string name of the Status
 func (s Status) LocaleString(lang translation.Locale) string {
-	return lang.Tr("actions.status." + s.String())
+	return lang.TrString("actions.status." + s.String())
 }
 
 // IsDone returns whether the Status is final
diff --git a/models/actions/task.go b/models/actions/task.go
index 96a6d2e80c..9946cf5233 100644
--- a/models/actions/task.go
+++ b/models/actions/task.go
@@ -11,6 +11,7 @@ import (
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
@@ -227,7 +228,9 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
 	if runner.RepoID != 0 {
 		jobCond = builder.Eq{"repo_id": runner.RepoID}
 	} else if runner.OwnerID != 0 {
-		jobCond = builder.In("repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": runner.OwnerID}))
+		jobCond = builder.In("repo_id", builder.Select("`repository`.id").From("repository").
+			Join("INNER", "repo_unit", "`repository`.id = `repo_unit`.repo_id").
+			Where(builder.Eq{"`repository`.owner_id": runner.OwnerID, "`repo_unit`.type": unit.TypeActions}))
 	}
 	if jobCond.IsValid() {
 		jobCond = builder.In("run_id", builder.Select("id").From("action_run").Where(jobCond))
diff --git a/models/actions/task_list.go b/models/actions/task_list.go
index b07d00b8db..5e17f91441 100644
--- a/models/actions/task_list.go
+++ b/models/actions/task_list.go
@@ -16,14 +16,9 @@ import (
 type TaskList []*ActionTask
 
 func (tasks TaskList) GetJobIDs() []int64 {
-	ids := make(container.Set[int64], len(tasks))
-	for _, t := range tasks {
-		if t.JobID == 0 {
-			continue
-		}
-		ids.Add(t.JobID)
-	}
-	return ids.Values()
+	return container.FilterSlice(tasks, func(t *ActionTask) (int64, bool) {
+		return t.JobID, t.JobID != 0
+	})
 }
 
 func (tasks TaskList) LoadJobs(ctx context.Context) error {
diff --git a/models/actions/variable.go b/models/actions/variable.go
index 12717e0ae4..b0a455e675 100644
--- a/models/actions/variable.go
+++ b/models/actions/variable.go
@@ -6,12 +6,11 @@ package actions
 import (
 	"context"
 	"errors"
-	"fmt"
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 
 	"xorm.io/builder"
 )
@@ -54,24 +53,24 @@ type FindVariablesOpts struct {
 	db.ListOptions
 	OwnerID int64
 	RepoID  int64
+	Name    string
 }
 
 func (opts FindVariablesOpts) ToConds() builder.Cond {
 	cond := builder.NewCond()
+	// Since we now support instance-level variables,
+	// there is no need to check for null values for `owner_id` and `repo_id`
 	cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
 	cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
+
+	if opts.Name != "" {
+		cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)})
+	}
 	return cond
 }
 
-func GetVariableByID(ctx context.Context, variableID int64) (*ActionVariable, error) {
-	var variable ActionVariable
-	has, err := db.GetEngine(ctx).Where("id=?", variableID).Get(&variable)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return nil, fmt.Errorf("variable with id %d: %w", variableID, util.ErrNotExist)
-	}
-	return &variable, nil
+func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariable, error) {
+	return db.Find[ActionVariable](ctx, opts)
 }
 
 func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
@@ -82,3 +81,42 @@ func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error)
 		})
 	return count != 0, err
 }
+
+func DeleteVariable(ctx context.Context, id int64) error {
+	if _, err := db.DeleteByID[ActionVariable](ctx, id); err != nil {
+		return err
+	}
+	return nil
+}
+
+func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) {
+	variables := map[string]string{}
+
+	// Global
+	globalVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{})
+	if err != nil {
+		log.Error("find global variables: %v", err)
+		return nil, err
+	}
+
+	// Org / User level
+	ownerVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{OwnerID: run.Repo.OwnerID})
+	if err != nil {
+		log.Error("find variables of org: %d, error: %v", run.Repo.OwnerID, err)
+		return nil, err
+	}
+
+	// Repo level
+	repoVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{RepoID: run.RepoID})
+	if err != nil {
+		log.Error("find variables of repo: %d, error: %v", run.RepoID, err)
+		return nil, err
+	}
+
+	// Level precedence: Repo > Org / User > Global
+	for _, v := range append(globalVariables, append(ownerVariables, repoVariables...)...) {
+		variables[v.Name] = v.Data
+	}
+
+	return variables, nil
+}
diff --git a/models/activities/action.go b/models/activities/action.go
index 15bd9a52ac..7e2ef4c9ae 100644
--- a/models/activities/action.go
+++ b/models/activities/action.go
@@ -148,6 +148,7 @@ type Action struct {
 	Repo        *repo_model.Repository `xorm:"-"`
 	CommentID   int64                  `xorm:"INDEX"`
 	Comment     *issues_model.Comment  `xorm:"-"`
+	Issue       *issues_model.Issue    `xorm:"-"` // get the issue id from content
 	IsDeleted   bool                   `xorm:"NOT NULL DEFAULT false"`
 	RefName     string
 	IsPrivate   bool               `xorm:"NOT NULL DEFAULT false"`
@@ -225,8 +226,8 @@ func (a *Action) ShortActUserName(ctx context.Context) string {
 	return base.EllipsisString(a.GetActUserName(ctx), 20)
 }
 
-// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
-func (a *Action) GetDisplayName(ctx context.Context) string {
+// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
+func (a *Action) GetActDisplayName(ctx context.Context) string {
 	if setting.UI.DefaultShowFullName {
 		trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx))
 		if len(trimmedFullName) > 0 {
@@ -236,8 +237,8 @@ func (a *Action) GetDisplayName(ctx context.Context) string {
 	return a.ShortActUserName(ctx)
 }
 
-// GetDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
-func (a *Action) GetDisplayNameTitle(ctx context.Context) string {
+// GetActDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
+func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
 	if setting.UI.DefaultShowFullName {
 		return a.ShortActUserName(ctx)
 	}
@@ -290,11 +291,6 @@ func (a *Action) GetRepoAbsoluteLink(ctx context.Context) string {
 	return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx))
 }
 
-// GetCommentHTMLURL returns link to action comment.
-func (a *Action) GetCommentHTMLURL(ctx context.Context) string {
-	return a.getCommentHTMLURL(ctx)
-}
-
 func (a *Action) loadComment(ctx context.Context) (err error) {
 	if a.CommentID == 0 || a.Comment != nil {
 		return nil
@@ -303,7 +299,8 @@ func (a *Action) loadComment(ctx context.Context) (err error) {
 	return err
 }
 
-func (a *Action) getCommentHTMLURL(ctx context.Context) string {
+// GetCommentHTMLURL returns link to action comment.
+func (a *Action) GetCommentHTMLURL(ctx context.Context) string {
 	if a == nil {
 		return "#"
 	}
@@ -311,34 +308,19 @@ func (a *Action) getCommentHTMLURL(ctx context.Context) string {
 	if a.Comment != nil {
 		return a.Comment.HTMLURL(ctx)
 	}
-	if len(a.GetIssueInfos()) == 0 {
+
+	if err := a.LoadIssue(ctx); err != nil || a.Issue == nil {
 		return "#"
 	}
-	// Return link to issue
-	issueIDString := a.GetIssueInfos()[0]
-	issueID, err := strconv.ParseInt(issueIDString, 10, 64)
-	if err != nil {
+	if err := a.Issue.LoadRepo(ctx); err != nil {
 		return "#"
 	}
 
-	issue, err := issues_model.GetIssueByID(ctx, issueID)
-	if err != nil {
-		return "#"
-	}
-
-	if err = issue.LoadRepo(ctx); err != nil {
-		return "#"
-	}
-
-	return issue.HTMLURL()
+	return a.Issue.HTMLURL()
 }
 
 // GetCommentLink returns link to action comment.
 func (a *Action) GetCommentLink(ctx context.Context) string {
-	return a.getCommentLink(ctx)
-}
-
-func (a *Action) getCommentLink(ctx context.Context) string {
 	if a == nil {
 		return "#"
 	}
@@ -346,26 +328,15 @@ func (a *Action) getCommentLink(ctx context.Context) string {
 	if a.Comment != nil {
 		return a.Comment.Link(ctx)
 	}
-	if len(a.GetIssueInfos()) == 0 {
+
+	if err := a.LoadIssue(ctx); err != nil || a.Issue == nil {
 		return "#"
 	}
-	// Return link to issue
-	issueIDString := a.GetIssueInfos()[0]
-	issueID, err := strconv.ParseInt(issueIDString, 10, 64)
-	if err != nil {
+	if err := a.Issue.LoadRepo(ctx); err != nil {
 		return "#"
 	}
 
-	issue, err := issues_model.GetIssueByID(ctx, issueID)
-	if err != nil {
-		return "#"
-	}
-
-	if err = issue.LoadRepo(ctx); err != nil {
-		return "#"
-	}
-
-	return issue.Link()
+	return a.Issue.Link()
 }
 
 // GetBranch returns the action's repository branch.
@@ -393,33 +364,66 @@ func (a *Action) GetCreate() time.Time {
 	return a.CreatedUnix.AsTime()
 }
 
-// GetIssueInfos returns a list of issues associated with
-// the action.
+func (a *Action) IsIssueEvent() bool {
+	return a.OpType.InActions("comment_issue", "approve_pull_request", "reject_pull_request", "comment_pull", "merge_pull_request")
+}
+
+// GetIssueInfos returns a list of associated information with the action.
 func (a *Action) GetIssueInfos() []string {
-	return strings.SplitN(a.Content, "|", 3)
+	// make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length
+	ret := strings.SplitN(a.Content, "|", 3)
+	for len(ret) < 3 {
+		ret = append(ret, "")
+	}
+	return ret
+}
+
+func (a *Action) getIssueIndex() int64 {
+	infos := a.GetIssueInfos()
+	if len(infos) == 0 {
+		return 0
+	}
+	index, _ := strconv.ParseInt(infos[0], 10, 64)
+	return index
+}
+
+func (a *Action) LoadIssue(ctx context.Context) error {
+	if a.Issue != nil {
+		return nil
+	}
+	if index := a.getIssueIndex(); index > 0 {
+		issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index)
+		if err != nil {
+			return err
+		}
+		a.Issue = issue
+		a.Issue.Repo = a.Repo
+	}
+	return nil
 }
 
 // GetIssueTitle returns the title of first issue associated with the action.
 func (a *Action) GetIssueTitle(ctx context.Context) string {
-	index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
-	issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index)
-	if err != nil {
-		log.Error("GetIssueByIndex: %v", err)
-		return "500 when get issue"
+	if err := a.LoadIssue(ctx); err != nil {
+		log.Error("LoadIssue: %v", err)
+		return "<500 when get issue>"
 	}
-	return issue.Title
+	if a.Issue == nil {
+		return "<Issue not found>"
+	}
+	return a.Issue.Title
 }
 
-// GetIssueContent returns the content of first issue associated with
-// this action.
+// GetIssueContent returns the content of first issue associated with this action.
 func (a *Action) GetIssueContent(ctx context.Context) string {
-	index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
-	issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index)
-	if err != nil {
-		log.Error("GetIssueByIndex: %v", err)
-		return "500 when get issue"
+	if err := a.LoadIssue(ctx); err != nil {
+		log.Error("LoadIssue: %v", err)
+		return "<500 when get issue>"
 	}
-	return issue.Content
+	if a.Issue == nil {
+		return "<Content not found>"
+	}
+	return a.Issue.Content
 }
 
 // GetFeedsOptions options for retrieving feeds
@@ -459,7 +463,7 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
 		return nil, 0, fmt.Errorf("FindAndCount: %w", err)
 	}
 
-	if err := ActionList(actions).loadAttributes(ctx); err != nil {
+	if err := ActionList(actions).LoadAttributes(ctx); err != nil {
 		return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
 	}
 
diff --git a/models/activities/action_list.go b/models/activities/action_list.go
index 3d74397c69..6e23b173b5 100644
--- a/models/activities/action_list.go
+++ b/models/activities/action_list.go
@@ -6,25 +6,28 @@ package activities
 import (
 	"context"
 	"fmt"
+	"strconv"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/util"
+
+	"xorm.io/builder"
 )
 
 // ActionList defines a list of actions
 type ActionList []*Action
 
 func (actions ActionList) getUserIDs() []int64 {
-	userIDs := make(container.Set[int64], len(actions))
-	for _, action := range actions {
-		userIDs.Add(action.ActUserID)
-	}
-	return userIDs.Values()
+	return container.FilterSlice(actions, func(action *Action) (int64, bool) {
+		return action.ActUserID, true
+	})
 }
 
-func (actions ActionList) loadUsers(ctx context.Context) (map[int64]*user_model.User, error) {
+func (actions ActionList) LoadActUsers(ctx context.Context) (map[int64]*user_model.User, error) {
 	if len(actions) == 0 {
 		return nil, nil
 	}
@@ -45,14 +48,12 @@ func (actions ActionList) loadUsers(ctx context.Context) (map[int64]*user_model.
 }
 
 func (actions ActionList) getRepoIDs() []int64 {
-	repoIDs := make(container.Set[int64], len(actions))
-	for _, action := range actions {
-		repoIDs.Add(action.RepoID)
-	}
-	return repoIDs.Values()
+	return container.FilterSlice(actions, func(action *Action) (int64, bool) {
+		return action.RepoID, true
+	})
 }
 
-func (actions ActionList) loadRepositories(ctx context.Context) error {
+func (actions ActionList) LoadRepositories(ctx context.Context) error {
 	if len(actions) == 0 {
 		return nil
 	}
@@ -63,11 +64,11 @@ func (actions ActionList) loadRepositories(ctx context.Context) error {
 	if err != nil {
 		return fmt.Errorf("find repository: %w", err)
 	}
-
 	for _, action := range actions {
 		action.Repo = repoMaps[action.RepoID]
 	}
-	return nil
+	repos := repo_model.RepositoryList(util.ValuesOfMap(repoMaps))
+	return repos.LoadUnits(ctx)
 }
 
 func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]*user_model.User) (err error) {
@@ -75,37 +76,122 @@ func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]*
 		userMap = make(map[int64]*user_model.User)
 	}
 
-	for _, action := range actions {
+	missingUserIDs := container.FilterSlice(actions, func(action *Action) (int64, bool) {
 		if action.Repo == nil {
-			continue
+			return 0, false
 		}
-		repoOwner, ok := userMap[action.Repo.OwnerID]
-		if !ok {
-			repoOwner, err = user_model.GetUserByID(ctx, action.Repo.OwnerID)
-			if err != nil {
-				if user_model.IsErrUserNotExist(err) {
-					continue
-				}
-				return err
-			}
-			userMap[repoOwner.ID] = repoOwner
+		_, alreadyLoaded := userMap[action.Repo.OwnerID]
+		return action.Repo.OwnerID, !alreadyLoaded
+	})
+
+	if err := db.GetEngine(ctx).
+		In("id", missingUserIDs).
+		Find(&userMap); err != nil {
+		return fmt.Errorf("find user: %w", err)
+	}
+
+	for _, action := range actions {
+		if action.Repo != nil {
+			action.Repo.Owner = userMap[action.Repo.OwnerID]
 		}
-		action.Repo.Owner = repoOwner
 	}
 
 	return nil
 }
 
-// loadAttributes loads all attributes
-func (actions ActionList) loadAttributes(ctx context.Context) error {
-	userMap, err := actions.loadUsers(ctx)
+// LoadAttributes loads all attributes
+func (actions ActionList) LoadAttributes(ctx context.Context) error {
+	// the load sequence cannot be changed because of the dependencies
+	userMap, err := actions.LoadActUsers(ctx)
 	if err != nil {
 		return err
 	}
-
-	if err := actions.loadRepositories(ctx); err != nil {
+	if err := actions.LoadRepositories(ctx); err != nil {
 		return err
 	}
-
-	return actions.loadRepoOwner(ctx, userMap)
+	if err := actions.loadRepoOwner(ctx, userMap); err != nil {
+		return err
+	}
+	if err := actions.LoadIssues(ctx); err != nil {
+		return err
+	}
+	return actions.LoadComments(ctx)
+}
+
+func (actions ActionList) LoadComments(ctx context.Context) error {
+	if len(actions) == 0 {
+		return nil
+	}
+
+	commentIDs := make([]int64, 0, len(actions))
+	for _, action := range actions {
+		if action.CommentID > 0 {
+			commentIDs = append(commentIDs, action.CommentID)
+		}
+	}
+
+	commentsMap := make(map[int64]*issues_model.Comment, len(commentIDs))
+	if err := db.GetEngine(ctx).In("id", commentIDs).Find(&commentsMap); err != nil {
+		return fmt.Errorf("find comment: %w", err)
+	}
+
+	for _, action := range actions {
+		if action.CommentID > 0 {
+			action.Comment = commentsMap[action.CommentID]
+			if action.Comment != nil {
+				action.Comment.Issue = action.Issue
+			}
+		}
+	}
+	return nil
+}
+
+func (actions ActionList) LoadIssues(ctx context.Context) error {
+	if len(actions) == 0 {
+		return nil
+	}
+
+	conditions := builder.NewCond()
+	issueNum := 0
+	for _, action := range actions {
+		if action.IsIssueEvent() {
+			infos := action.GetIssueInfos()
+			if len(infos) == 0 {
+				continue
+			}
+			index, _ := strconv.ParseInt(infos[0], 10, 64)
+			if index > 0 {
+				conditions = conditions.Or(builder.Eq{
+					"repo_id": action.RepoID,
+					"`index`": index,
+				})
+				issueNum++
+			}
+		}
+	}
+	if !conditions.IsValid() {
+		return nil
+	}
+
+	issuesMap := make(map[string]*issues_model.Issue, issueNum)
+	issues := make([]*issues_model.Issue, 0, issueNum)
+	if err := db.GetEngine(ctx).Where(conditions).Find(&issues); err != nil {
+		return fmt.Errorf("find issue: %w", err)
+	}
+	for _, issue := range issues {
+		issuesMap[fmt.Sprintf("%d-%d", issue.RepoID, issue.Index)] = issue
+	}
+
+	for _, action := range actions {
+		if !action.IsIssueEvent() {
+			continue
+		}
+		if index := action.getIssueIndex(); index > 0 {
+			if issue, ok := issuesMap[fmt.Sprintf("%d-%d", action.RepoID, index)]; ok {
+				action.Issue = issue
+				action.Issue.Repo = action.Repo
+			}
+		}
+	}
+	return nil
 }
diff --git a/models/activities/notification.go b/models/activities/notification.go
index 230bcdd6e8..dc1b8c6fae 100644
--- a/models/activities/notification.go
+++ b/models/activities/notification.go
@@ -12,12 +12,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
-	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 
@@ -79,53 +75,6 @@ func init() {
 	db.RegisterModel(new(Notification))
 }
 
-// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
-type FindNotificationOptions struct {
-	db.ListOptions
-	UserID            int64
-	RepoID            int64
-	IssueID           int64
-	Status            []NotificationStatus
-	Source            []NotificationSource
-	UpdatedAfterUnix  int64
-	UpdatedBeforeUnix int64
-}
-
-// ToCond will convert each condition into a xorm-Cond
-func (opts FindNotificationOptions) ToConds() builder.Cond {
-	cond := builder.NewCond()
-	if opts.UserID != 0 {
-		cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
-	}
-	if opts.RepoID != 0 {
-		cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
-	}
-	if opts.IssueID != 0 {
-		cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
-	}
-	if len(opts.Status) > 0 {
-		if len(opts.Status) == 1 {
-			cond = cond.And(builder.Eq{"notification.status": opts.Status[0]})
-		} else {
-			cond = cond.And(builder.In("notification.status", opts.Status))
-		}
-	}
-	if len(opts.Source) > 0 {
-		cond = cond.And(builder.In("notification.source", opts.Source))
-	}
-	if opts.UpdatedAfterUnix != 0 {
-		cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
-	}
-	if opts.UpdatedBeforeUnix != 0 {
-		cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
-	}
-	return cond
-}
-
-func (opts FindNotificationOptions) ToOrders() string {
-	return "notification.updated_unix DESC"
-}
-
 // CreateRepoTransferNotification creates  notification for the user a repository was transferred to
 func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error {
 	return db.WithTx(ctx, func(ctx context.Context) error {
@@ -159,109 +108,6 @@ func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_mo
 	})
 }
 
-// CreateOrUpdateIssueNotifications creates an issue notification
-// for each watcher, or updates it if already exists
-// receiverID > 0 just send to receiver, else send to all watcher
-func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
-	ctx, committer, err := db.TxContext(ctx)
-	if err != nil {
-		return err
-	}
-	defer committer.Close()
-
-	if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil {
-		return err
-	}
-
-	return committer.Commit()
-}
-
-func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
-	// init
-	var toNotify container.Set[int64]
-	notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
-		IssueID: issueID,
-	})
-	if err != nil {
-		return err
-	}
-
-	issue, err := issues_model.GetIssueByID(ctx, issueID)
-	if err != nil {
-		return err
-	}
-
-	if receiverID > 0 {
-		toNotify = make(container.Set[int64], 1)
-		toNotify.Add(receiverID)
-	} else {
-		toNotify = make(container.Set[int64], 32)
-		issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true)
-		if err != nil {
-			return err
-		}
-		toNotify.AddMultiple(issueWatches...)
-		if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
-			repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
-			if err != nil {
-				return err
-			}
-			toNotify.AddMultiple(repoWatches...)
-		}
-		issueParticipants, err := issue.GetParticipantIDsByIssue(ctx)
-		if err != nil {
-			return err
-		}
-		toNotify.AddMultiple(issueParticipants...)
-
-		// dont notify user who cause notification
-		delete(toNotify, notificationAuthorID)
-		// explicit unwatch on issue
-		issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false)
-		if err != nil {
-			return err
-		}
-		for _, id := range issueUnWatches {
-			toNotify.Remove(id)
-		}
-	}
-
-	err = issue.LoadRepo(ctx)
-	if err != nil {
-		return err
-	}
-
-	// notify
-	for userID := range toNotify {
-		issue.Repo.Units = nil
-		user, err := user_model.GetUserByID(ctx, userID)
-		if err != nil {
-			if user_model.IsErrUserNotExist(err) {
-				continue
-			}
-
-			return err
-		}
-		if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) {
-			continue
-		}
-		if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) {
-			continue
-		}
-
-		if notificationExists(notifications, issue.ID, userID) {
-			if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil {
-				return err
-			}
-			continue
-		}
-		if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
 func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error {
 	notification := &Notification{
 		UserID:    userID,
@@ -449,309 +295,6 @@ func GetUIDsAndNotificationCounts(ctx context.Context, since, until timeutil.Tim
 	return res, db.GetEngine(ctx).SQL(sql, since, until, NotificationStatusUnread).Find(&res)
 }
 
-// NotificationList contains a list of notifications
-type NotificationList []*Notification
-
-// LoadAttributes load Repo Issue User and Comment if not loaded
-func (nl NotificationList) LoadAttributes(ctx context.Context) error {
-	if _, _, err := nl.LoadRepos(ctx); err != nil {
-		return err
-	}
-	if _, err := nl.LoadIssues(ctx); err != nil {
-		return err
-	}
-	if _, err := nl.LoadUsers(ctx); err != nil {
-		return err
-	}
-	if _, err := nl.LoadComments(ctx); err != nil {
-		return err
-	}
-	return nil
-}
-
-func (nl NotificationList) getPendingRepoIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.Repository != nil {
-			continue
-		}
-		ids.Add(notification.RepoID)
-	}
-	return ids.Values()
-}
-
-// LoadRepos loads repositories from database
-func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) {
-	if len(nl) == 0 {
-		return repo_model.RepositoryList{}, []int{}, nil
-	}
-
-	repoIDs := nl.getPendingRepoIDs()
-	repos := make(map[int64]*repo_model.Repository, len(repoIDs))
-	left := len(repoIDs)
-	for left > 0 {
-		limit := db.DefaultMaxInSize
-		if left < limit {
-			limit = left
-		}
-		rows, err := db.GetEngine(ctx).
-			In("id", repoIDs[:limit]).
-			Rows(new(repo_model.Repository))
-		if err != nil {
-			return nil, nil, err
-		}
-
-		for rows.Next() {
-			var repo repo_model.Repository
-			err = rows.Scan(&repo)
-			if err != nil {
-				rows.Close()
-				return nil, nil, err
-			}
-
-			repos[repo.ID] = &repo
-		}
-		_ = rows.Close()
-
-		left -= limit
-		repoIDs = repoIDs[limit:]
-	}
-
-	failed := []int{}
-
-	reposList := make(repo_model.RepositoryList, 0, len(repoIDs))
-	for i, notification := range nl {
-		if notification.Repository == nil {
-			notification.Repository = repos[notification.RepoID]
-		}
-		if notification.Repository == nil {
-			log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID)
-			failed = append(failed, i)
-			continue
-		}
-		var found bool
-		for _, r := range reposList {
-			if r.ID == notification.RepoID {
-				found = true
-				break
-			}
-		}
-		if !found {
-			reposList = append(reposList, notification.Repository)
-		}
-	}
-	return reposList, failed, nil
-}
-
-func (nl NotificationList) getPendingIssueIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.Issue != nil {
-			continue
-		}
-		ids.Add(notification.IssueID)
-	}
-	return ids.Values()
-}
-
-// LoadIssues loads issues from database
-func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) {
-	if len(nl) == 0 {
-		return []int{}, nil
-	}
-
-	issueIDs := nl.getPendingIssueIDs()
-	issues := make(map[int64]*issues_model.Issue, len(issueIDs))
-	left := len(issueIDs)
-	for left > 0 {
-		limit := db.DefaultMaxInSize
-		if left < limit {
-			limit = left
-		}
-		rows, err := db.GetEngine(ctx).
-			In("id", issueIDs[:limit]).
-			Rows(new(issues_model.Issue))
-		if err != nil {
-			return nil, err
-		}
-
-		for rows.Next() {
-			var issue issues_model.Issue
-			err = rows.Scan(&issue)
-			if err != nil {
-				rows.Close()
-				return nil, err
-			}
-
-			issues[issue.ID] = &issue
-		}
-		_ = rows.Close()
-
-		left -= limit
-		issueIDs = issueIDs[limit:]
-	}
-
-	failures := []int{}
-
-	for i, notification := range nl {
-		if notification.Issue == nil {
-			notification.Issue = issues[notification.IssueID]
-			if notification.Issue == nil {
-				if notification.IssueID != 0 {
-					log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID)
-					failures = append(failures, i)
-				}
-				continue
-			}
-			notification.Issue.Repo = notification.Repository
-		}
-	}
-	return failures, nil
-}
-
-// Without returns the notification list without the failures
-func (nl NotificationList) Without(failures []int) NotificationList {
-	if len(failures) == 0 {
-		return nl
-	}
-	remaining := make([]*Notification, 0, len(nl))
-	last := -1
-	var i int
-	for _, i = range failures {
-		remaining = append(remaining, nl[last+1:i]...)
-		last = i
-	}
-	if len(nl) > i {
-		remaining = append(remaining, nl[i+1:]...)
-	}
-	return remaining
-}
-
-func (nl NotificationList) getPendingCommentIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.CommentID == 0 || notification.Comment != nil {
-			continue
-		}
-		ids.Add(notification.CommentID)
-	}
-	return ids.Values()
-}
-
-func (nl NotificationList) getUserIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.UserID == 0 || notification.User != nil {
-			continue
-		}
-		ids.Add(notification.UserID)
-	}
-	return ids.Values()
-}
-
-// LoadUsers loads users from database
-func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) {
-	if len(nl) == 0 {
-		return []int{}, nil
-	}
-
-	userIDs := nl.getUserIDs()
-	users := make(map[int64]*user_model.User, len(userIDs))
-	left := len(userIDs)
-	for left > 0 {
-		limit := db.DefaultMaxInSize
-		if left < limit {
-			limit = left
-		}
-		rows, err := db.GetEngine(ctx).
-			In("id", userIDs[:limit]).
-			Rows(new(user_model.User))
-		if err != nil {
-			return nil, err
-		}
-
-		for rows.Next() {
-			var user user_model.User
-			err = rows.Scan(&user)
-			if err != nil {
-				rows.Close()
-				return nil, err
-			}
-
-			users[user.ID] = &user
-		}
-		_ = rows.Close()
-
-		left -= limit
-		userIDs = userIDs[limit:]
-	}
-
-	failures := []int{}
-	for i, notification := range nl {
-		if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil {
-			notification.User = users[notification.UserID]
-			if notification.User == nil {
-				log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID)
-				failures = append(failures, i)
-				continue
-			}
-		}
-	}
-	return failures, nil
-}
-
-// LoadComments loads comments from database
-func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
-	if len(nl) == 0 {
-		return []int{}, nil
-	}
-
-	commentIDs := nl.getPendingCommentIDs()
-	comments := make(map[int64]*issues_model.Comment, len(commentIDs))
-	left := len(commentIDs)
-	for left > 0 {
-		limit := db.DefaultMaxInSize
-		if left < limit {
-			limit = left
-		}
-		rows, err := db.GetEngine(ctx).
-			In("id", commentIDs[:limit]).
-			Rows(new(issues_model.Comment))
-		if err != nil {
-			return nil, err
-		}
-
-		for rows.Next() {
-			var comment issues_model.Comment
-			err = rows.Scan(&comment)
-			if err != nil {
-				rows.Close()
-				return nil, err
-			}
-
-			comments[comment.ID] = &comment
-		}
-		_ = rows.Close()
-
-		left -= limit
-		commentIDs = commentIDs[limit:]
-	}
-
-	failures := []int{}
-	for i, notification := range nl {
-		if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil {
-			notification.Comment = comments[notification.CommentID]
-			if notification.Comment == nil {
-				log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID)
-				failures = append(failures, i)
-				continue
-			}
-			notification.Comment.Issue = notification.Issue
-		}
-	}
-	return failures, nil
-}
-
 // SetIssueReadBy sets issue to be read by given user.
 func SetIssueReadBy(ctx context.Context, issueID, userID int64) error {
 	if err := issues_model.UpdateIssueUserByRead(ctx, userID, issueID); err != nil {
diff --git a/models/activities/notification_list.go b/models/activities/notification_list.go
new file mode 100644
index 0000000000..0cbb91df3c
--- /dev/null
+++ b/models/activities/notification_list.go
@@ -0,0 +1,499 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package activities
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	access_model "code.gitea.io/gitea/models/perm/access"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unit"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
+
+	"xorm.io/builder"
+)
+
+// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
+type FindNotificationOptions struct {
+	db.ListOptions
+	UserID            int64
+	RepoID            int64
+	IssueID           int64
+	Status            []NotificationStatus
+	Source            []NotificationSource
+	UpdatedAfterUnix  int64
+	UpdatedBeforeUnix int64
+}
+
+// ToCond will convert each condition into a xorm-Cond
+func (opts FindNotificationOptions) ToConds() builder.Cond {
+	cond := builder.NewCond()
+	if opts.UserID != 0 {
+		cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
+	}
+	if opts.RepoID != 0 {
+		cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
+	}
+	if opts.IssueID != 0 {
+		cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
+	}
+	if len(opts.Status) > 0 {
+		if len(opts.Status) == 1 {
+			cond = cond.And(builder.Eq{"notification.status": opts.Status[0]})
+		} else {
+			cond = cond.And(builder.In("notification.status", opts.Status))
+		}
+	}
+	if len(opts.Source) > 0 {
+		cond = cond.And(builder.In("notification.source", opts.Source))
+	}
+	if opts.UpdatedAfterUnix != 0 {
+		cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
+	}
+	if opts.UpdatedBeforeUnix != 0 {
+		cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
+	}
+	return cond
+}
+
+func (opts FindNotificationOptions) ToOrders() string {
+	return "notification.updated_unix DESC"
+}
+
+// CreateOrUpdateIssueNotifications creates an issue notification
+// for each watcher, or updates it if already exists
+// receiverID > 0 just send to receiver, else send to all watcher
+func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
+	ctx, committer, err := db.TxContext(ctx)
+	if err != nil {
+		return err
+	}
+	defer committer.Close()
+
+	if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil {
+		return err
+	}
+
+	return committer.Commit()
+}
+
+func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
+	// init
+	var toNotify container.Set[int64]
+	notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
+		IssueID: issueID,
+	})
+	if err != nil {
+		return err
+	}
+
+	issue, err := issues_model.GetIssueByID(ctx, issueID)
+	if err != nil {
+		return err
+	}
+
+	if receiverID > 0 {
+		toNotify = make(container.Set[int64], 1)
+		toNotify.Add(receiverID)
+	} else {
+		toNotify = make(container.Set[int64], 32)
+		issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true)
+		if err != nil {
+			return err
+		}
+		toNotify.AddMultiple(issueWatches...)
+		if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
+			repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
+			if err != nil {
+				return err
+			}
+			toNotify.AddMultiple(repoWatches...)
+		}
+		issueParticipants, err := issue.GetParticipantIDsByIssue(ctx)
+		if err != nil {
+			return err
+		}
+		toNotify.AddMultiple(issueParticipants...)
+
+		// dont notify user who cause notification
+		delete(toNotify, notificationAuthorID)
+		// explicit unwatch on issue
+		issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false)
+		if err != nil {
+			return err
+		}
+		for _, id := range issueUnWatches {
+			toNotify.Remove(id)
+		}
+	}
+
+	err = issue.LoadRepo(ctx)
+	if err != nil {
+		return err
+	}
+
+	// notify
+	for userID := range toNotify {
+		issue.Repo.Units = nil
+		user, err := user_model.GetUserByID(ctx, userID)
+		if err != nil {
+			if user_model.IsErrUserNotExist(err) {
+				continue
+			}
+
+			return err
+		}
+		if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) {
+			continue
+		}
+		if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) {
+			continue
+		}
+
+		if notificationExists(notifications, issue.ID, userID) {
+			if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil {
+				return err
+			}
+			continue
+		}
+		if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// NotificationList contains a list of notifications
+type NotificationList []*Notification
+
+// LoadAttributes load Repo Issue User and Comment if not loaded
+func (nl NotificationList) LoadAttributes(ctx context.Context) error {
+	if _, _, err := nl.LoadRepos(ctx); err != nil {
+		return err
+	}
+	if _, err := nl.LoadIssues(ctx); err != nil {
+		return err
+	}
+	if _, err := nl.LoadUsers(ctx); err != nil {
+		return err
+	}
+	if _, err := nl.LoadComments(ctx); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (nl NotificationList) getPendingRepoIDs() []int64 {
+	return container.FilterSlice(nl, func(n *Notification) (int64, bool) {
+		if n.Repository != nil {
+			return 0, false
+		}
+		return n.RepoID, true
+	})
+}
+
+// LoadRepos loads repositories from database
+func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) {
+	if len(nl) == 0 {
+		return repo_model.RepositoryList{}, []int{}, nil
+	}
+
+	repoIDs := nl.getPendingRepoIDs()
+	repos := make(map[int64]*repo_model.Repository, len(repoIDs))
+	left := len(repoIDs)
+	for left > 0 {
+		limit := db.DefaultMaxInSize
+		if left < limit {
+			limit = left
+		}
+		rows, err := db.GetEngine(ctx).
+			In("id", repoIDs[:limit]).
+			Rows(new(repo_model.Repository))
+		if err != nil {
+			return nil, nil, err
+		}
+
+		for rows.Next() {
+			var repo repo_model.Repository
+			err = rows.Scan(&repo)
+			if err != nil {
+				rows.Close()
+				return nil, nil, err
+			}
+
+			repos[repo.ID] = &repo
+		}
+		_ = rows.Close()
+
+		left -= limit
+		repoIDs = repoIDs[limit:]
+	}
+
+	failed := []int{}
+
+	reposList := make(repo_model.RepositoryList, 0, len(repoIDs))
+	for i, notification := range nl {
+		if notification.Repository == nil {
+			notification.Repository = repos[notification.RepoID]
+		}
+		if notification.Repository == nil {
+			log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID)
+			failed = append(failed, i)
+			continue
+		}
+		var found bool
+		for _, r := range reposList {
+			if r.ID == notification.RepoID {
+				found = true
+				break
+			}
+		}
+		if !found {
+			reposList = append(reposList, notification.Repository)
+		}
+	}
+	return reposList, failed, nil
+}
+
+func (nl NotificationList) getPendingIssueIDs() []int64 {
+	ids := make(container.Set[int64], len(nl))
+	for _, notification := range nl {
+		if notification.Issue != nil {
+			continue
+		}
+		ids.Add(notification.IssueID)
+	}
+	return ids.Values()
+}
+
+// LoadIssues loads issues from database
+func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) {
+	if len(nl) == 0 {
+		return []int{}, nil
+	}
+
+	issueIDs := nl.getPendingIssueIDs()
+	issues := make(map[int64]*issues_model.Issue, len(issueIDs))
+	left := len(issueIDs)
+	for left > 0 {
+		limit := db.DefaultMaxInSize
+		if left < limit {
+			limit = left
+		}
+		rows, err := db.GetEngine(ctx).
+			In("id", issueIDs[:limit]).
+			Rows(new(issues_model.Issue))
+		if err != nil {
+			return nil, err
+		}
+
+		for rows.Next() {
+			var issue issues_model.Issue
+			err = rows.Scan(&issue)
+			if err != nil {
+				rows.Close()
+				return nil, err
+			}
+
+			issues[issue.ID] = &issue
+		}
+		_ = rows.Close()
+
+		left -= limit
+		issueIDs = issueIDs[limit:]
+	}
+
+	failures := []int{}
+
+	for i, notification := range nl {
+		if notification.Issue == nil {
+			notification.Issue = issues[notification.IssueID]
+			if notification.Issue == nil {
+				if notification.IssueID != 0 {
+					log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID)
+					failures = append(failures, i)
+				}
+				continue
+			}
+			notification.Issue.Repo = notification.Repository
+		}
+	}
+	return failures, nil
+}
+
+// Without returns the notification list without the failures
+func (nl NotificationList) Without(failures []int) NotificationList {
+	if len(failures) == 0 {
+		return nl
+	}
+	remaining := make([]*Notification, 0, len(nl))
+	last := -1
+	var i int
+	for _, i = range failures {
+		remaining = append(remaining, nl[last+1:i]...)
+		last = i
+	}
+	if len(nl) > i {
+		remaining = append(remaining, nl[i+1:]...)
+	}
+	return remaining
+}
+
+func (nl NotificationList) getPendingCommentIDs() []int64 {
+	ids := make(container.Set[int64], len(nl))
+	for _, notification := range nl {
+		if notification.CommentID == 0 || notification.Comment != nil {
+			continue
+		}
+		ids.Add(notification.CommentID)
+	}
+	return ids.Values()
+}
+
+func (nl NotificationList) getUserIDs() []int64 {
+	ids := make(container.Set[int64], len(nl))
+	for _, notification := range nl {
+		if notification.UserID == 0 || notification.User != nil {
+			continue
+		}
+		ids.Add(notification.UserID)
+	}
+	return ids.Values()
+}
+
+// LoadUsers loads users from database
+func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) {
+	if len(nl) == 0 {
+		return []int{}, nil
+	}
+
+	userIDs := nl.getUserIDs()
+	users := make(map[int64]*user_model.User, len(userIDs))
+	left := len(userIDs)
+	for left > 0 {
+		limit := db.DefaultMaxInSize
+		if left < limit {
+			limit = left
+		}
+		rows, err := db.GetEngine(ctx).
+			In("id", userIDs[:limit]).
+			Rows(new(user_model.User))
+		if err != nil {
+			return nil, err
+		}
+
+		for rows.Next() {
+			var user user_model.User
+			err = rows.Scan(&user)
+			if err != nil {
+				rows.Close()
+				return nil, err
+			}
+
+			users[user.ID] = &user
+		}
+		_ = rows.Close()
+
+		left -= limit
+		userIDs = userIDs[limit:]
+	}
+
+	failures := []int{}
+	for i, notification := range nl {
+		if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil {
+			notification.User = users[notification.UserID]
+			if notification.User == nil {
+				log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID)
+				failures = append(failures, i)
+				continue
+			}
+		}
+	}
+	return failures, nil
+}
+
+// LoadComments loads comments from database
+func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
+	if len(nl) == 0 {
+		return []int{}, nil
+	}
+
+	commentIDs := nl.getPendingCommentIDs()
+	comments := make(map[int64]*issues_model.Comment, len(commentIDs))
+	left := len(commentIDs)
+	for left > 0 {
+		limit := db.DefaultMaxInSize
+		if left < limit {
+			limit = left
+		}
+		rows, err := db.GetEngine(ctx).
+			In("id", commentIDs[:limit]).
+			Rows(new(issues_model.Comment))
+		if err != nil {
+			return nil, err
+		}
+
+		for rows.Next() {
+			var comment issues_model.Comment
+			err = rows.Scan(&comment)
+			if err != nil {
+				rows.Close()
+				return nil, err
+			}
+
+			comments[comment.ID] = &comment
+		}
+		_ = rows.Close()
+
+		left -= limit
+		commentIDs = commentIDs[limit:]
+	}
+
+	failures := []int{}
+	for i, notification := range nl {
+		if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil {
+			notification.Comment = comments[notification.CommentID]
+			if notification.Comment == nil {
+				log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID)
+				failures = append(failures, i)
+				continue
+			}
+			notification.Comment.Issue = notification.Issue
+		}
+	}
+	return failures, nil
+}
+
+// LoadIssuePullRequests loads all issues' pull requests if possible
+func (nl NotificationList) LoadIssuePullRequests(ctx context.Context) error {
+	issues := make(map[int64]*issues_model.Issue, len(nl))
+	for _, notification := range nl {
+		if notification.Issue != nil && notification.Issue.IsPull && notification.Issue.PullRequest == nil {
+			issues[notification.Issue.ID] = notification.Issue
+		}
+	}
+
+	if len(issues) == 0 {
+		return nil
+	}
+
+	pulls, err := issues_model.GetPullRequestByIssueIDs(ctx, util.KeysOfMap(issues))
+	if err != nil {
+		return err
+	}
+
+	for _, pull := range pulls {
+		if issue := issues[pull.IssueID]; issue != nil {
+			issue.PullRequest = pull
+			issue.PullRequest.Issue = issue
+		}
+	}
+
+	return nil
+}
diff --git a/models/activities/statistic.go b/models/activities/statistic.go
index fe5f7d0872..d1a459d1b2 100644
--- a/models/activities/statistic.go
+++ b/models/activities/statistic.go
@@ -9,6 +9,7 @@ import (
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
@@ -29,7 +30,8 @@ type Statistic struct {
 		Mirror, Release, AuthSource, Webhook,
 		Milestone, Label, HookTask,
 		Team, UpdateTask, Project,
-		ProjectBoard, Attachment int64
+		ProjectBoard, Attachment,
+		Branches, Tags, CommitStatus int64
 		IssueByLabel      []IssueByLabelCount
 		IssueByRepository []IssueByRepositoryCount
 	}
@@ -58,6 +60,9 @@ func GetStatistic(ctx context.Context) (stats Statistic) {
 	stats.Counter.Watch, _ = e.Count(new(repo_model.Watch))
 	stats.Counter.Star, _ = e.Count(new(repo_model.Star))
 	stats.Counter.Access, _ = e.Count(new(access_model.Access))
+	stats.Counter.Branches, _ = e.Count(new(git_model.Branch))
+	stats.Counter.Tags, _ = e.Where("is_draft=?", false).Count(new(repo_model.Release))
+	stats.Counter.CommitStatus, _ = e.Count(new(git_model.CommitStatus))
 
 	type IssueCount struct {
 		Count    int64
diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go
index 83fbab5d36..06ac31bc6f 100644
--- a/models/asymkey/gpg_key_commit_verification.go
+++ b/models/asymkey/gpg_key_commit_verification.go
@@ -139,13 +139,7 @@ func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerific
 		}
 	}
 
-	keyID := ""
-	if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
-		keyID = fmt.Sprintf("%X", *sig.IssuerKeyId)
-	}
-	if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
-		keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20])
-	}
+	keyID := tryGetKeyIDFromSignature(sig)
 	defaultReason := NoKeyFound
 
 	// First check if the sig has a keyID and if so just look at that
diff --git a/models/asymkey/gpg_key_common.go b/models/asymkey/gpg_key_common.go
index b02be2851a..9c015582f1 100644
--- a/models/asymkey/gpg_key_common.go
+++ b/models/asymkey/gpg_key_common.go
@@ -134,3 +134,13 @@ func extractSignature(s string) (*packet.Signature, error) {
 	}
 	return sig, nil
 }
+
+func tryGetKeyIDFromSignature(sig *packet.Signature) string {
+	if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
+		return fmt.Sprintf("%016X", *sig.IssuerKeyId)
+	}
+	if sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
+		return fmt.Sprintf("%016X", sig.IssuerFingerprint[12:20])
+	}
+	return ""
+}
diff --git a/models/asymkey/gpg_key_test.go b/models/asymkey/gpg_key_test.go
index dee74bc281..d3fbb01d82 100644
--- a/models/asymkey/gpg_key_test.go
+++ b/models/asymkey/gpg_key_test.go
@@ -11,7 +11,9 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
 
+	"github.com/keybase/go-crypto/openpgp/packet"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -391,3 +393,13 @@ epiDVQ==
 		assert.Equal(t, time.Unix(1586105389, 0), expire)
 	}
 }
+
+func TestTryGetKeyIDFromSignature(t *testing.T) {
+	assert.Empty(t, tryGetKeyIDFromSignature(&packet.Signature{}))
+	assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{
+		IssuerKeyId: util.ToPointer(uint64(0x38D1A3EADDBEA9C)),
+	}))
+	assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{
+		IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c},
+	}))
+}
diff --git a/models/asymkey/gpg_key_verify.go b/models/asymkey/gpg_key_verify.go
index 4cf46ab556..01812a2d54 100644
--- a/models/asymkey/gpg_key_verify.go
+++ b/models/asymkey/gpg_key_verify.go
@@ -46,6 +46,10 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st
 		return "", ErrGPGKeyNotExist{}
 	}
 
+	if err := key.LoadSubKeys(ctx); err != nil {
+		return "", err
+	}
+
 	sig, err := extractSignature(signature)
 	if err != nil {
 		return "", ErrGPGInvalidTokenSignature{
diff --git a/models/asymkey/ssh_key_authorized_keys.go b/models/asymkey/ssh_key_authorized_keys.go
index 267ab252c8..2e4cd62e5c 100644
--- a/models/asymkey/ssh_key_authorized_keys.go
+++ b/models/asymkey/ssh_key_authorized_keys.go
@@ -12,7 +12,6 @@ import (
 	"path/filepath"
 	"strings"
 	"sync"
-	"time"
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
@@ -44,6 +43,12 @@ const (
 
 var sshOpLocker sync.Mutex
 
+func WithSSHOpLocker(f func() error) error {
+	sshOpLocker.Lock()
+	defer sshOpLocker.Unlock()
+	return f()
+}
+
 // AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
 func AuthorizedStringForKey(key *PublicKey) string {
 	sb := &strings.Builder{}
@@ -114,65 +119,6 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
 	return nil
 }
 
-// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
-// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
-// outside any session scope independently.
-func RewriteAllPublicKeys(ctx context.Context) error {
-	// Don't rewrite key if internal server
-	if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
-		return nil
-	}
-
-	sshOpLocker.Lock()
-	defer sshOpLocker.Unlock()
-
-	if setting.SSH.RootPath != "" {
-		// First of ensure that the RootPath is present, and if not make it with 0700 permissions
-		// This of course doesn't guarantee that this is the right directory for authorized_keys
-		// but at least if it's supposed to be this directory and it doesn't exist and we're the
-		// right user it will at least be created properly.
-		err := os.MkdirAll(setting.SSH.RootPath, 0o700)
-		if err != nil {
-			log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
-			return err
-		}
-	}
-
-	fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
-	tmpPath := fPath + ".tmp"
-	t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
-	if err != nil {
-		return err
-	}
-	defer func() {
-		t.Close()
-		if err := util.Remove(tmpPath); err != nil {
-			log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err)
-		}
-	}()
-
-	if setting.SSH.AuthorizedKeysBackup {
-		isExist, err := util.IsExist(fPath)
-		if err != nil {
-			log.Error("Unable to check if %s exists. Error: %v", fPath, err)
-			return err
-		}
-		if isExist {
-			bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
-			if err = util.CopyFile(fPath, bakPath); err != nil {
-				return err
-			}
-		}
-	}
-
-	if err := RegeneratePublicKeys(ctx, t); err != nil {
-		return err
-	}
-
-	t.Close()
-	return util.Rename(tmpPath, fPath)
-}
-
 // RegeneratePublicKeys regenerates the authorized_keys file
 func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
 	if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
@@ -193,6 +139,8 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
 		if err != nil {
 			return err
 		}
+		defer f.Close()
+
 		scanner := bufio.NewScanner(f)
 		for scanner.Scan() {
 			line := scanner.Text()
@@ -202,11 +150,12 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
 			}
 			_, err = t.WriteString(line + "\n")
 			if err != nil {
-				f.Close()
 				return err
 			}
 		}
-		f.Close()
+		if err = scanner.Err(); err != nil {
+			return fmt.Errorf("RegeneratePublicKeys scan: %w", err)
+		}
 	}
 	return nil
 }
diff --git a/models/asymkey/ssh_key_fingerprint.go b/models/asymkey/ssh_key_fingerprint.go
index b9cfb1b251..1ed3b5df2a 100644
--- a/models/asymkey/ssh_key_fingerprint.go
+++ b/models/asymkey/ssh_key_fingerprint.go
@@ -76,23 +76,14 @@ func calcFingerprintNative(publicKeyContent string) (string, error) {
 // CalcFingerprint calculate public key's fingerprint
 func CalcFingerprint(publicKeyContent string) (string, error) {
 	// Call the method based on configuration
-	var (
-		fnName, fp string
-		err        error
-	)
-	if len(setting.SSH.KeygenPath) == 0 {
-		fnName = "calcFingerprintNative"
-		fp, err = calcFingerprintNative(publicKeyContent)
-	} else {
-		fnName = "calcFingerprintSSHKeygen"
-		fp, err = calcFingerprintSSHKeygen(publicKeyContent)
-	}
+	useNative := setting.SSH.KeygenPath == ""
+	calcFn := util.Iif(useNative, calcFingerprintNative, calcFingerprintSSHKeygen)
+	fp, err := calcFn(publicKeyContent)
 	if err != nil {
 		if IsErrKeyUnableVerify(err) {
-			log.Info("%s", publicKeyContent)
 			return "", err
 		}
-		return "", fmt.Errorf("%s: %w", fnName, err)
+		return "", fmt.Errorf("CalcFingerprint(%s): %w", util.Iif(useNative, "native", "ssh-keygen"), err)
 	}
 	return fp, nil
 }
diff --git a/models/asymkey/ssh_key_principals.go b/models/asymkey/ssh_key_principals.go
index 4e7dee2c91..e8b97d306e 100644
--- a/models/asymkey/ssh_key_principals.go
+++ b/models/asymkey/ssh_key_principals.go
@@ -9,51 +9,11 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/models/perm"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 )
 
-// AddPrincipalKey adds new principal to database and authorized_principals file.
-func AddPrincipalKey(ctx context.Context, ownerID int64, content string, authSourceID int64) (*PublicKey, error) {
-	dbCtx, committer, err := db.TxContext(ctx)
-	if err != nil {
-		return nil, err
-	}
-	defer committer.Close()
-
-	// Principals cannot be duplicated.
-	has, err := db.GetEngine(dbCtx).
-		Where("content = ? AND type = ?", content, KeyTypePrincipal).
-		Get(new(PublicKey))
-	if err != nil {
-		return nil, err
-	} else if has {
-		return nil, ErrKeyAlreadyExist{0, "", content}
-	}
-
-	key := &PublicKey{
-		OwnerID:       ownerID,
-		Name:          content,
-		Content:       content,
-		Mode:          perm.AccessModeWrite,
-		Type:          KeyTypePrincipal,
-		LoginSourceID: authSourceID,
-	}
-	if err = db.Insert(dbCtx, key); err != nil {
-		return nil, fmt.Errorf("addKey: %w", err)
-	}
-
-	if err = committer.Commit(); err != nil {
-		return nil, err
-	}
-
-	committer.Close()
-
-	return key, RewriteAllPrincipalKeys(ctx)
-}
-
 // CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines
 func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content string) (_ string, err error) {
 	if setting.SSH.Disabled {
diff --git a/models/auth/source.go b/models/auth/source.go
index 1bdde8235c..f360ca9801 100644
--- a/models/auth/source.go
+++ b/models/auth/source.go
@@ -11,6 +11,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
@@ -243,14 +244,14 @@ func CreateSource(ctx context.Context, source *Source) error {
 
 type FindSourcesOptions struct {
 	db.ListOptions
-	IsActive  util.OptionalBool
+	IsActive  optional.Option[bool]
 	LoginType Type
 }
 
 func (opts FindSourcesOptions) ToConds() builder.Cond {
 	conds := builder.NewCond()
-	if !opts.IsActive.IsNone() {
-		conds = conds.And(builder.Eq{"is_active": opts.IsActive.IsTrue()})
+	if opts.IsActive.Has() {
+		conds = conds.And(builder.Eq{"is_active": opts.IsActive.Value()})
 	}
 	if opts.LoginType != NoType {
 		conds = conds.And(builder.Eq{"`type`": opts.LoginType})
@@ -262,7 +263,7 @@ func (opts FindSourcesOptions) ToConds() builder.Cond {
 // source of type LoginSSPI
 func IsSSPIEnabled(ctx context.Context) bool {
 	exist, err := db.Exist[Source](ctx, FindSourcesOptions{
-		IsActive:  util.OptionalBoolTrue,
+		IsActive:  optional.Some(true),
 		LoginType: SSPI,
 	}.ToConds())
 	if err != nil {
diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go
index 51061e5205..d0c341a192 100644
--- a/models/auth/twofactor.go
+++ b/models/auth/twofactor.go
@@ -6,6 +6,7 @@ package auth
 import (
 	"context"
 	"crypto/md5"
+	"crypto/sha256"
 	"crypto/subtle"
 	"encoding/base32"
 	"encoding/base64"
@@ -18,7 +19,6 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
-	"github.com/minio/sha256-simd"
 	"github.com/pquerna/otp/totp"
 	"golang.org/x/crypto/pbkdf2"
 )
diff --git a/models/avatars/avatar.go b/models/avatars/avatar.go
index bbe16483bf..9c56e0f9a0 100644
--- a/models/avatars/avatar.go
+++ b/models/avatars/avatar.go
@@ -24,7 +24,7 @@ import (
 
 const (
 	// DefaultAvatarClass is the default class of a rendered avatar
-	DefaultAvatarClass = "ui avatar gt-vm"
+	DefaultAvatarClass = "ui avatar tw-align-middle"
 	// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
 	DefaultAvatarPixelSize = 28
 )
diff --git a/models/db/collation.go b/models/db/collation.go
index 2f5ff2bf05..c128cf5029 100644
--- a/models/db/collation.go
+++ b/models/db/collation.go
@@ -166,8 +166,7 @@ func preprocessDatabaseCollation(x *xorm.Engine) {
 
 	// try to alter database collation to expected if the database is empty, it might fail in some cases (and it isn't necessary to succeed)
 	// at the moment, there is no "altering" solution for MSSQL, site admin should manually change the database collation
-	// and there is a bug https://github.com/go-testfixtures/testfixtures/pull/182 mssql: Invalid object name 'information_schema.tables'.
-	if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) && r.ExistingTableNumber == 0 && x.Dialect().URI().DBType == schemas.MYSQL {
+	if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) && r.ExistingTableNumber == 0 {
 		if err = alterDatabaseCollation(x, r.ExpectedCollation); err != nil {
 			log.Error("Failed to change database collation to %q: %v", r.ExpectedCollation, err)
 		} else {
diff --git a/models/db/consistency.go b/models/db/consistency.go
index d19732cf80..d0b0ab8315 100644
--- a/models/db/consistency.go
+++ b/models/db/consistency.go
@@ -10,21 +10,21 @@ import (
 )
 
 // CountOrphanedObjects count subjects with have no existing refobject anymore
-func CountOrphanedObjects(ctx context.Context, subject, refobject, joinCond string) (int64, error) {
+func CountOrphanedObjects(ctx context.Context, subject, refObject, joinCond string) (int64, error) {
 	return GetEngine(ctx).
 		Table("`"+subject+"`").
-		Join("LEFT", "`"+refobject+"`", joinCond).
-		Where(builder.IsNull{"`" + refobject + "`.id"}).
+		Join("LEFT", "`"+refObject+"`", joinCond).
+		Where(builder.IsNull{"`" + refObject + "`.id"}).
 		Select("COUNT(`" + subject + "`.`id`)").
 		Count()
 }
 
 // DeleteOrphanedObjects delete subjects with have no existing refobject anymore
-func DeleteOrphanedObjects(ctx context.Context, subject, refobject, joinCond string) error {
+func DeleteOrphanedObjects(ctx context.Context, subject, refObject, joinCond string) error {
 	subQuery := builder.Select("`"+subject+"`.id").
 		From("`"+subject+"`").
-		Join("LEFT", "`"+refobject+"`", joinCond).
-		Where(builder.IsNull{"`" + refobject + "`.id"})
+		Join("LEFT", "`"+refObject+"`", joinCond).
+		Where(builder.IsNull{"`" + refObject + "`.id"})
 	b := builder.Delete(builder.In("id", subQuery)).From("`" + subject + "`")
 	_, err := GetEngine(ctx).Exec(b)
 	return err
diff --git a/models/db/context.go b/models/db/context.go
index cda608af19..43f612518a 100644
--- a/models/db/context.go
+++ b/models/db/context.go
@@ -120,6 +120,16 @@ func (c *halfCommitter) Close() error {
 
 // TxContext represents a transaction Context,
 // it will reuse the existing transaction in the parent context or create a new one.
+// Some tips to use:
+//
+//	1 It's always recommended to use `WithTx` in new code instead of `TxContext`, since `WithTx` will handle the transaction automatically.
+//	2. To maintain the old code which uses `TxContext`:
+//	  a. Always call `Close()` before returning regardless of whether `Commit()` has been called.
+//	  b. Always call `Commit()` before returning if there are no errors, even if the code did not change any data.
+//	  c. Remember the `Committer` will be a halfCommitter when a transaction is being reused.
+//	     So calling `Commit()` will do nothing, but calling `Close()` without calling `Commit()` will rollback the transaction.
+//	     And all operations submitted by the caller stack will be rollbacked as well, not only the operations in the current function.
+//	  d. It doesn't mean rollback is forbidden, but always do it only when there is an error, and you do want to rollback.
 func TxContext(parentCtx context.Context) (*Context, Committer, error) {
 	if sess, ok := inTransaction(parentCtx); ok {
 		return newContext(parentCtx, sess, true), &halfCommitter{committer: sess}, nil
diff --git a/models/db/engine.go b/models/db/engine.go
index 2cd1c36c58..8684c4e2f1 100755
--- a/models/db/engine.go
+++ b/models/db/engine.go
@@ -11,16 +11,19 @@ import (
 	"io"
 	"reflect"
 	"strings"
+	"time"
 
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 
 	"xorm.io/xorm"
+	"xorm.io/xorm/contexts"
 	"xorm.io/xorm/names"
 	"xorm.io/xorm/schemas"
 
-	_ "github.com/denisenkom/go-mssqldb" // Needed for the MSSQL driver
-	_ "github.com/go-sql-driver/mysql"   // Needed for the MySQL driver
-	_ "github.com/lib/pq"                // Needed for the Postgresql driver
+	_ "github.com/go-sql-driver/mysql"  // Needed for the MySQL driver
+	_ "github.com/lib/pq"               // Needed for the Postgresql driver
+	_ "github.com/microsoft/go-mssqldb" // Needed for the MSSQL driver
 )
 
 var (
@@ -143,6 +146,13 @@ func InitEngine(ctx context.Context) error {
 	xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
 	xormEngine.SetDefaultContext(ctx)
 
+	if setting.Database.SlowQueryThreshold > 0 {
+		xormEngine.AddHook(&SlowQueryHook{
+			Threshold: setting.Database.SlowQueryThreshold,
+			Logger:    log.GetLogger("xorm"),
+		})
+	}
+
 	SetDefaultEngine(ctx, xormEngine)
 	return nil
 }
@@ -298,3 +308,24 @@ func SetLogSQL(ctx context.Context, on bool) {
 		sess.Engine().ShowSQL(on)
 	}
 }
+
+type SlowQueryHook struct {
+	Threshold time.Duration
+	Logger    log.Logger
+}
+
+var _ contexts.Hook = &SlowQueryHook{}
+
+func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
+	return c.Ctx, nil
+}
+
+func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
+	if c.ExecuteTime >= h.Threshold {
+		// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function
+		// is being displayed (the function that ultimately wants to execute the query in the code)
+		// instead of the function of the slow query hook being called.
+		h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
+	}
+	return nil
+}
diff --git a/models/error.go b/models/error.go
index 83dfe29805..75c53245de 100644
--- a/models/error.go
+++ b/models/error.go
@@ -493,6 +493,23 @@ func (err ErrMergeUnrelatedHistories) Error() string {
 	return fmt.Sprintf("Merge UnrelatedHistories Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
 }
 
+// ErrMergeDivergingFastForwardOnly represents an error if a fast-forward-only merge fails because the branches diverge
+type ErrMergeDivergingFastForwardOnly struct {
+	StdOut string
+	StdErr string
+	Err    error
+}
+
+// IsErrMergeDivergingFastForwardOnly checks if an error is a ErrMergeDivergingFastForwardOnly.
+func IsErrMergeDivergingFastForwardOnly(err error) bool {
+	_, ok := err.(ErrMergeDivergingFastForwardOnly)
+	return ok
+}
+
+func (err ErrMergeDivergingFastForwardOnly) Error() string {
+	return fmt.Sprintf("Merge DivergingFastForwardOnly Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
+}
+
 // ErrRebaseConflicts represents an error if rebase fails with a conflict
 type ErrRebaseConflicts struct {
 	Style     repo_model.MergeStyle
diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml
index 1bb6a9a8ac..4171e31fef 100644
--- a/models/fixtures/access.yml
+++ b/models/fixtures/access.yml
@@ -42,96 +42,132 @@
 
 -
   id: 8
-  user_id: 15
+  user_id: 10
   repo_id: 21
   mode: 2
 
 -
   id: 9
-  user_id: 15
-  repo_id: 22
+  user_id: 10
+  repo_id: 32
   mode: 2
 
 -
   id: 10
   user_id: 15
+  repo_id: 21
+  mode: 2
+
+-
+  id: 11
+  user_id: 15
+  repo_id: 22
+  mode: 2
+
+-
+  id: 12
+  user_id: 15
   repo_id: 23
   mode: 4
 
 -
-  id: 11
+  id: 13
   user_id: 15
   repo_id: 24
   mode: 4
 
 -
-  id: 12
+  id: 14
   user_id: 15
   repo_id: 32
   mode: 2
 
 -
-  id: 13
+  id: 15
   user_id: 18
   repo_id: 21
   mode: 2
 
 -
-  id: 14
+  id: 16
   user_id: 18
   repo_id: 22
   mode: 2
 
 -
-  id: 15
+  id: 17
   user_id: 18
   repo_id: 23
   mode: 4
 
 -
-  id: 16
+  id: 18
   user_id: 18
   repo_id: 24
   mode: 4
 
 -
-  id: 17
+  id: 19
   user_id: 20
   repo_id: 24
   mode: 1
 
 -
-  id: 18
+  id: 20
   user_id: 20
   repo_id: 27
   mode: 4
 
 -
-  id: 19
+  id: 21
   user_id: 20
   repo_id: 28
   mode: 4
 
 -
-  id: 20
+  id: 22
   user_id: 29
   repo_id: 4
   mode: 2
 
 -
-  id: 21
+  id: 23
   user_id: 29
   repo_id: 24
   mode: 1
 
 -
-  id: 22
+  id: 24
   user_id: 31
   repo_id: 27
   mode: 4
 
 -
-  id: 23
+  id: 25
   user_id: 31
   repo_id: 28
   mode: 4
+
+-
+  id: 26
+  user_id: 38
+  repo_id: 60
+  mode: 2
+
+-
+  id: 27
+  user_id: 38
+  repo_id: 61
+  mode: 1
+
+-
+  id: 28
+  user_id: 39
+  repo_id: 61
+  mode: 1
+
+-
+  id: 29
+  user_id: 40
+  repo_id: 61
+  mode: 4
diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml
index 2c2151f354..a42ab77ca5 100644
--- a/models/fixtures/action_run.yml
+++ b/models/fixtures/action_run.yml
@@ -17,3 +17,22 @@
   updated: 1683636626
   need_approval: 0
   approved_by: 0
+-
+  id: 792
+  title: "update actions"
+  repo_id: 4
+  owner_id: 1
+  workflow_id: "artifact.yaml"
+  index: 188
+  trigger_user_id: 1
+  ref: "refs/heads/master"
+  commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
+  event: "push"
+  is_fork_pull_request: 0
+  status: 1
+  started: 1683636528
+  stopped: 1683636626
+  created: 1683636108
+  updated: 1683636626
+  need_approval: 0
+  approved_by: 0
diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml
index 071998b979..fd90f4fd5d 100644
--- a/models/fixtures/action_run_job.yml
+++ b/models/fixtures/action_run_job.yml
@@ -12,3 +12,17 @@
   status: 1
   started: 1683636528
   stopped: 1683636626
+-
+  id: 193
+  run_id: 792
+  repo_id: 4
+  owner_id: 1
+  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
+  is_fork_pull_request: 0
+  name: job_2
+  attempt: 1
+  job_id: job_2
+  task_id: 48
+  status: 1
+  started: 1683636528
+  stopped: 1683636626
diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml
index c78fb3c5d6..443effe08c 100644
--- a/models/fixtures/action_task.yml
+++ b/models/fixtures/action_task.yml
@@ -18,3 +18,23 @@
   log_length: 707
   log_size: 90179
   log_expired: 0
+-
+  id: 48
+  job_id: 193
+  attempt: 1
+  runner_id: 1
+  status: 6 # 6 is the status code for "running", running task can upload artifacts
+  started: 1683636528
+  stopped: 1683636626
+  repo_id: 4
+  owner_id: 1
+  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
+  is_fork_pull_request: 0
+  token_hash: ffffcfffffffbffffffffffffffffefffffffafffffffffffffffffffffffffffffdffffffffffffffffffffffffffffffff
+  token_salt: ffffffffff
+  token_last_eight: ffffffff
+  log_filename: artifact-test2/2f/47.log
+  log_in_storage: 1
+  log_length: 707
+  log_size: 90179
+  log_expired: 0
diff --git a/models/fixtures/collaboration.yml b/models/fixtures/collaboration.yml
index ef77d22b24..4c3ac367f6 100644
--- a/models/fixtures/collaboration.yml
+++ b/models/fixtures/collaboration.yml
@@ -45,3 +45,21 @@
   repo_id: 22
   user_id: 18
   mode: 2 # write
+
+-
+  id: 9
+  repo_id: 60
+  user_id: 38
+  mode: 2 # write
+
+-
+  id: 10
+  repo_id: 21
+  user_id: 10
+  mode: 2 # write
+
+-
+  id: 11
+  repo_id: 32
+  user_id: 10
+  mode: 2 # write
diff --git a/models/fixtures/comment.yml b/models/fixtures/comment.yml
index 17586caa21..74fc716180 100644
--- a/models/fixtures/comment.yml
+++ b/models/fixtures/comment.yml
@@ -75,3 +75,11 @@
   content: "comment in private pository"
   created_unix: 946684811
   updated_unix: 946684811
+
+-
+  id: 9
+  type: 22 # review
+  poster_id: 2
+  issue_id: 2 # in repo_id 1
+  review_id: 20
+  created_unix: 946684810
diff --git a/models/fixtures/email_address.yml b/models/fixtures/email_address.yml
index 67a99f43e2..b2a0432635 100644
--- a/models/fixtures/email_address.yml
+++ b/models/fixtures/email_address.yml
@@ -293,3 +293,27 @@
   lower_email: user37@example.com
   is_activated: true
   is_primary: true
+
+-
+  id: 38
+  uid: 38
+  email: user38@example.com
+  lower_email: user38@example.com
+  is_activated: true
+  is_primary: true
+
+-
+  id: 39
+  uid: 39
+  email: user39@example.com
+  lower_email: user39@example.com
+  is_activated: true
+  is_primary: true
+
+-
+  id: 40
+  uid: 40
+  email: user40@example.com
+  lower_email: user40@example.com
+  is_activated: true
+  is_primary: true
diff --git a/models/fixtures/hook_task.yml b/models/fixtures/hook_task.yml
index 6dbb10151a..d573406b36 100644
--- a/models/fixtures/hook_task.yml
+++ b/models/fixtures/hook_task.yml
@@ -3,3 +3,35 @@
   hook_id: 1
   uuid: uuid1
   is_delivered: true
+  is_succeed: false
+  request_content: >
+    {
+      "url": "/matrix-delivered",
+      "http_method":"PUT",
+      "headers": {
+        "X-Head": "42"
+      },
+      "body": "{}"
+    }
+
+-
+  id: 2
+  hook_id: 1
+  uuid: uuid2
+  is_delivered: false
+
+-
+  id: 3
+  hook_id: 1
+  uuid: uuid3
+  is_delivered: true
+  is_succeed: true
+  payload_content: '{"key":"value"}' # legacy task, payload saved in payload_content (and not in request_content)
+  request_content: >
+    {
+      "url": "/matrix-success",
+      "http_method":"PUT",
+      "headers": {
+        "X-Head": "42"
+      }
+    }
diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml
index 0c9b6ff406..ca5b1c6cd1 100644
--- a/models/fixtures/issue.yml
+++ b/models/fixtures/issue.yml
@@ -338,3 +338,37 @@
   created_unix: 978307210
   updated_unix: 978307210
   is_locked: false
+
+-
+  id: 21
+  repo_id: 60
+  index: 1
+  poster_id: 39
+  original_author_id: 0
+  name: repo60 pull1
+  content: content for the 1st issue
+  milestone_id: 0
+  priority: 0
+  is_closed: false
+  is_pull: true
+  num_comments: 0
+  created_unix: 1707270422
+  updated_unix: 1707270422
+  is_locked: false
+
+-
+  id: 22
+  repo_id: 61
+  index: 1
+  poster_id: 40
+  original_author_id: 0
+  name: repo61 pull1
+  content: content for the 1st issue
+  milestone_id: 0
+  priority: 0
+  is_closed: false
+  is_pull: true
+  num_comments: 0
+  created_unix: 1707270422
+  updated_unix: 1707270422
+  is_locked: false
diff --git a/models/fixtures/issue_assignees.yml b/models/fixtures/issue_assignees.yml
index e5d36f921a..c40ecad676 100644
--- a/models/fixtures/issue_assignees.yml
+++ b/models/fixtures/issue_assignees.yml
@@ -14,3 +14,7 @@
   id: 4
   assignee_id: 2
   issue_id: 17
+-
+  id: 5
+  assignee_id: 10
+  issue_id: 6
diff --git a/models/fixtures/org_user.yml b/models/fixtures/org_user.yml
index 8d58169a32..a7fbcb2c5a 100644
--- a/models/fixtures/org_user.yml
+++ b/models/fixtures/org_user.yml
@@ -99,3 +99,21 @@
   uid: 5
   org_id: 36
   is_public: true
+
+-
+  id: 18
+  uid: 38
+  org_id: 41
+  is_public: true
+
+-
+  id: 19
+  uid: 39
+  org_id: 41
+  is_public: true
+
+-
+  id: 20
+  uid: 40
+  org_id: 41
+  is_public: true
diff --git a/models/fixtures/project.yml b/models/fixtures/project.yml
index 1bf8030f6a..44d87bce04 100644
--- a/models/fixtures/project.yml
+++ b/models/fixtures/project.yml
@@ -45,3 +45,27 @@
   type: 2
   created_unix: 1688973000
   updated_unix: 1688973000
+
+-
+  id: 5
+  title: project without default column
+  owner_id: 2
+  repo_id: 0
+  is_closed: false
+  creator_id: 2
+  board_type: 1
+  type: 2
+  created_unix: 1688973000
+  updated_unix: 1688973000
+
+-
+  id: 6
+  title: project with multiple default columns
+  owner_id: 2
+  repo_id: 0
+  is_closed: false
+  creator_id: 2
+  board_type: 1
+  type: 2
+  created_unix: 1688973000
+  updated_unix: 1688973000
diff --git a/models/fixtures/project_board.yml b/models/fixtures/project_board.yml
index dc4f9cf565..3293dea6ed 100644
--- a/models/fixtures/project_board.yml
+++ b/models/fixtures/project_board.yml
@@ -3,6 +3,7 @@
   project_id: 1
   title: To Do
   creator_id: 2
+  default: true
   created_unix: 1588117528
   updated_unix: 1588117528
 
@@ -29,3 +30,48 @@
   creator_id: 2
   created_unix: 1588117528
   updated_unix: 1588117528
+
+-
+  id: 5
+  project_id: 2
+  title: Backlog
+  creator_id: 2
+  default: true
+  created_unix: 1588117528
+  updated_unix: 1588117528
+
+-
+  id: 6
+  project_id: 4
+  title: Backlog
+  creator_id: 2
+  default: true
+  created_unix: 1588117528
+  updated_unix: 1588117528
+
+-
+  id: 7
+  project_id: 5
+  title: Done
+  creator_id: 2
+  default: false
+  created_unix: 1588117528
+  updated_unix: 1588117528
+
+-
+  id: 8
+  project_id: 6
+  title: Backlog
+  creator_id: 2
+  default: true
+  created_unix: 1588117528
+  updated_unix: 1588117528
+
+-
+  id: 9
+  project_id: 6
+  title: Uncategorized
+  creator_id: 2
+  default: true
+  created_unix: 1588117528
+  updated_unix: 1588117528
diff --git a/models/fixtures/pull_request.yml b/models/fixtures/pull_request.yml
index 560674c370..3fc8ce630d 100644
--- a/models/fixtures/pull_request.yml
+++ b/models/fixtures/pull_request.yml
@@ -9,6 +9,7 @@
   head_branch: branch1
   base_branch: master
   merge_base: 4a357436d925b5c974181ff12a994538ddc5a269
+  merged_commit_id: 1a8823cd1a9549fde083f992f6b9b87a7ab74fb3
   has_merged: true
   merger_id: 2
 
@@ -98,3 +99,21 @@
   index: 1
   head_repo_id: 23
   base_repo_id: 23
+
+-
+  id: 9
+  type: 0 # gitea pull request
+  status: 2 # mergable
+  issue_id: 21
+  index: 1
+  head_repo_id: 60
+  base_repo_id: 60
+
+-
+  id: 10
+  type: 0 # gitea pull request
+  status: 2 # mergable
+  issue_id: 22
+  index: 1
+  head_repo_id: 61
+  base_repo_id: 61
diff --git a/models/fixtures/repo_transfer.yml b/models/fixtures/repo_transfer.yml
index b841b5e983..db92c95248 100644
--- a/models/fixtures/repo_transfer.yml
+++ b/models/fixtures/repo_transfer.yml
@@ -5,3 +5,19 @@
   repo_id: 3
   created_unix: 1553610671
   updated_unix: 1553610671
+
+-
+  id: 2
+  doer_id: 16
+  recipient_id: 10
+  repo_id: 21
+  created_unix: 1553610671
+  updated_unix: 1553610671
+
+-
+  id: 3
+  doer_id: 3
+  recipient_id: 10
+  repo_id: 32
+  created_unix: 1553610671
+  updated_unix: 1553610671
diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml
index e6c59f527a..8a22db0445 100644
--- a/models/fixtures/repo_unit.yml
+++ b/models/fixtures/repo_unit.yml
@@ -650,12 +650,6 @@
   type: 2
   created_unix: 946684810
 
--
-  id: 98
-  repo_id: 1
-  type: 8
-  created_unix: 946684810
-
 -
   id: 99
   repo_id: 1
@@ -676,3 +670,45 @@
   type: 1
   config: "{}"
   created_unix: 946684810
+
+-
+  id: 102
+  repo_id: 60
+  type: 1
+  config: "{}"
+  created_unix: 946684810
+
+-
+  id: 103
+  repo_id: 60
+  type: 2
+  config: "{\"EnableTimetracker\":true,\"AllowOnlyContributorsToTrackTime\":true}"
+  created_unix: 946684810
+
+-
+  id: 104
+  repo_id: 60
+  type: 3
+  config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
+  created_unix: 946684810
+
+-
+  id: 105
+  repo_id: 61
+  type: 1
+  config: "{}"
+  created_unix: 946684810
+
+-
+  id: 106
+  repo_id: 61
+  type: 2
+  config: "{\"EnableTimetracker\":true,\"AllowOnlyContributorsToTrackTime\":true}"
+  created_unix: 946684810
+
+-
+  id: 107
+  repo_id: 61
+  type: 3
+  config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
+  created_unix: 946684810
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index f4e8376735..e5c6224c96 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -614,8 +614,8 @@
   owner_name: user16
   lower_name: big_test_public_3
   name: big_test_public_3
-  num_watches: 0
-  num_stars: 0
+  num_watches: 1
+  num_stars: 1
   num_forks: 0
   num_issues: 0
   num_closed_issues: 0
@@ -945,8 +945,8 @@
   owner_name: org3
   lower_name: repo21
   name: repo21
-  num_watches: 0
-  num_stars: 0
+  num_watches: 1
+  num_stars: 1
   num_forks: 0
   num_issues: 2
   num_closed_issues: 0
@@ -1706,3 +1706,65 @@
   is_private: true
   status: 0
   num_issues: 0
+
+-
+  id: 60
+  owner_id: 40
+  owner_name: user40
+  lower_name: repo60
+  name: repo60
+  default_branch: main
+  num_watches: 0
+  num_stars: 0
+  num_forks: 0
+  num_issues: 0
+  num_closed_issues: 0
+  num_pulls: 1
+  num_closed_pulls: 0
+  num_milestones: 0
+  num_closed_milestones: 0
+  num_projects: 0
+  num_closed_projects: 0
+  is_private: false
+  is_empty: false
+  is_archived: false
+  is_mirror: false
+  status: 0
+  is_fork: false
+  fork_id: 0
+  is_template: false
+  template_id: 0
+  size: 0
+  is_fsck_enabled: true
+  close_issues_via_commit_in_any_branch: false
+
+-
+  id: 61
+  owner_id: 41
+  owner_name: org41
+  lower_name: repo61
+  name: repo61
+  default_branch: main
+  num_watches: 0
+  num_stars: 0
+  num_forks: 0
+  num_issues: 0
+  num_closed_issues: 0
+  num_pulls: 1
+  num_closed_pulls: 0
+  num_milestones: 0
+  num_closed_milestones: 0
+  num_projects: 0
+  num_closed_projects: 0
+  is_private: false
+  is_empty: false
+  is_archived: false
+  is_mirror: false
+  status: 0
+  is_fork: false
+  fork_id: 0
+  is_template: false
+  template_id: 0
+  size: 0
+  is_fsck_enabled: true
+  close_issues_via_commit_in_any_branch: false
diff --git a/models/fixtures/review.yml b/models/fixtures/review.yml
index 7a88080068..ac97e24c2b 100644
--- a/models/fixtures/review.yml
+++ b/models/fixtures/review.yml
@@ -170,3 +170,12 @@
   content: "review request for user15"
   updated_unix: 946684835
   created_unix: 946684835
+
+-
+  id: 20
+  type: 22
+  reviewer_id: 1
+  issue_id: 2
+  content: "Review Comment"
+  updated_unix: 946684810
+  created_unix: 946684810
diff --git a/models/fixtures/star.yml b/models/fixtures/star.yml
index 860f26b8e2..39b51b3736 100644
--- a/models/fixtures/star.yml
+++ b/models/fixtures/star.yml
@@ -7,3 +7,13 @@
   id: 2
   uid: 2
   repo_id: 4
+
+-
+  id: 3
+  uid: 10
+  repo_id: 21
+
+-
+  id: 4
+  uid: 10
+  repo_id: 32
diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml
index 295e51e39c..149fe90888 100644
--- a/models/fixtures/team.yml
+++ b/models/fixtures/team.yml
@@ -217,3 +217,25 @@
   num_members: 1
   includes_all_repositories: false
   can_create_org_repo: true
+
+-
+  id: 21
+  org_id: 41
+  lower_name: owners
+  name: Owners
+  authorize: 4 # owner
+  num_repos: 1
+  num_members: 1
+  includes_all_repositories: true
+  can_create_org_repo: true
+
+-
+  id: 22
+  org_id: 41
+  lower_name: team1
+  name: Team1
+  authorize: 1 # read
+  num_repos: 1
+  num_members: 2
+  includes_all_repositories: false
+  can_create_org_repo: false
diff --git a/models/fixtures/team_repo.yml b/models/fixtures/team_repo.yml
index 8497720892..a29078107e 100644
--- a/models/fixtures/team_repo.yml
+++ b/models/fixtures/team_repo.yml
@@ -63,3 +63,15 @@
   org_id: 17
   team_id: 9
   repo_id: 24
+
+-
+  id: 12
+  org_id: 41
+  team_id: 21
+  repo_id: 61
+
+-
+  id: 13
+  org_id: 41
+  team_id: 22
+  repo_id: 61
diff --git a/models/fixtures/team_unit.yml b/models/fixtures/team_unit.yml
index c5531aa57a..de0e8d738b 100644
--- a/models/fixtures/team_unit.yml
+++ b/models/fixtures/team_unit.yml
@@ -286,3 +286,39 @@
   team_id: 2
   type: 8
   access_mode: 2
+
+-
+  id: 49
+  team_id: 21
+  type: 1
+  access_mode: 4
+
+-
+  id: 50
+  team_id: 21
+  type: 2
+  access_mode: 4
+
+-
+  id: 51
+  team_id: 21
+  type: 3
+  access_mode: 4
+
+-
+  id: 52
+  team_id: 22
+  type: 1
+  access_mode: 1
+
+-
+  id: 53
+  team_id: 22
+  type: 2
+  access_mode: 1
+
+-
+  id: 54
+  team_id: 22
+  type: 3
+  access_mode: 1
diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml
index 9142fe609a..02d57ae644 100644
--- a/models/fixtures/team_user.yml
+++ b/models/fixtures/team_user.yml
@@ -129,3 +129,21 @@
   org_id: 17
   team_id: 9
   uid: 15
+
+-
+  id: 23
+  org_id: 41
+  team_id: 21
+  uid: 40
+
+-
+  id: 24
+  org_id: 41
+  team_id: 22
+  uid: 38
+
+-
+  id: 25
+  org_id: 41
+  team_id: 22
+  uid: 39
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index aa0daedd85..a3de535508 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -361,7 +361,7 @@
   use_custom_avatar: false
   num_followers: 0
   num_following: 0
-  num_stars: 0
+  num_stars: 2
   num_repos: 3
   num_teams: 0
   num_members: 0
@@ -1369,3 +1369,151 @@
   repo_admin_change_team_access: false
   theme: ""
   keep_activity_private: false
+
+-
+  id: 38
+  lower_name: user38
+  name: user38
+  full_name: User38
+  email: user38@example.com
+  keep_email_private: false
+  email_notifications_preference: enabled
+  passwd: ZogKvWdyEx:password
+  passwd_hash_algo: dummy
+  must_change_password: false
+  login_source: 0
+  login_name: user38
+  type: 0
+  salt: ZogKvWdyEx
+  max_repo_creation: -1
+  is_active: true
+  is_admin: false
+  is_restricted: false
+  allow_git_hook: false
+  allow_import_local: false
+  allow_create_organization: true
+  prohibit_login: false
+  avatar: avatar38
+  avatar_email: user38@example.com
+  use_custom_avatar: false
+  num_followers: 0
+  num_following: 0
+  num_stars: 0
+  num_repos: 0
+  num_teams: 0
+  num_members: 0
+  visibility: 0
+  repo_admin_change_team_access: false
+  theme: ""
+  keep_activity_private: false
+
+-
+  id: 39
+  lower_name: user39
+  name: user39
+  full_name: User39
+  email: user39@example.com
+  keep_email_private: false
+  email_notifications_preference: enabled
+  passwd: ZogKvWdyEx:password
+  passwd_hash_algo: dummy
+  must_change_password: false
+  login_source: 0
+  login_name: user39
+  type: 0
+  salt: ZogKvWdyEx
+  max_repo_creation: -1
+  is_active: true
+  is_admin: false
+  is_restricted: false
+  allow_git_hook: false
+  allow_import_local: false
+  allow_create_organization: true
+  prohibit_login: false
+  avatar: avatar39
+  avatar_email: user39@example.com
+  use_custom_avatar: false
+  num_followers: 0
+  num_following: 0
+  num_stars: 0
+  num_repos: 0
+  num_teams: 0
+  num_members: 0
+  visibility: 0
+  repo_admin_change_team_access: false
+  theme: ""
+  keep_activity_private: false
+
+-
+  id: 40
+  lower_name: user40
+  name: user40
+  full_name: User40
+  email: user40@example.com
+  keep_email_private: false
+  email_notifications_preference: onmention
+  passwd: ZogKvWdyEx:password
+  passwd_hash_algo: dummy
+  must_change_password: false
+  login_source: 0
+  login_name: user40
+  type: 0
+  salt: ZogKvWdyEx
+  max_repo_creation: -1
+  is_active: true
+  is_admin: false
+  is_restricted: false
+  allow_git_hook: false
+  allow_import_local: false
+  allow_create_organization: true
+  prohibit_login: false
+  avatar: avatar40
+  avatar_email: user40@example.com
+  use_custom_avatar: false
+  num_followers: 0
+  num_following: 0
+  num_stars: 0
+  num_repos: 1
+  num_teams: 0
+  num_members: 0
+  visibility: 0
+  repo_admin_change_team_access: false
+  theme: ""
+  keep_activity_private: false
+
+-
+  id: 41
+  lower_name: org41
+  name: org41
+  full_name: Org41
+  email: org41@example.com
+  keep_email_private: false
+  email_notifications_preference: onmention
+  passwd: ZogKvWdyEx:password
+  passwd_hash_algo: dummy
+  must_change_password: false
+  login_source: 0
+  login_name: org41
+  type: 1
+  salt: ZogKvWdyEx
+  max_repo_creation: -1
+  is_active: false
+  is_admin: false
+  is_restricted: false
+  allow_git_hook: false
+  allow_import_local: false
+  allow_create_organization: true
+  prohibit_login: false
+  avatar: avatar41
+  avatar_email: org41@example.com
+  use_custom_avatar: false
+  num_followers: 0
+  num_following: 0
+  num_stars: 0
+  num_repos: 1
+  num_teams: 2
+  num_members: 3
+  visibility: 0
+  repo_admin_change_team_access: false
+  theme: ""
+  keep_activity_private: false
diff --git a/models/fixtures/user_blocking.yml b/models/fixtures/user_blocking.yml
new file mode 100644
index 0000000000..2ec9d99df5
--- /dev/null
+++ b/models/fixtures/user_blocking.yml
@@ -0,0 +1,19 @@
+-
+  id: 1
+  blocker_id: 2
+  blockee_id: 29
+
+-
+  id: 2
+  blocker_id: 17
+  blockee_id: 28
+
+-
+  id: 3
+  blocker_id: 2
+  blockee_id: 34
+
+-
+  id: 4
+  blocker_id: 50
+  blockee_id: 34
diff --git a/models/fixtures/watch.yml b/models/fixtures/watch.yml
index 1950ac99e7..18bcd2ed2b 100644
--- a/models/fixtures/watch.yml
+++ b/models/fixtures/watch.yml
@@ -27,3 +27,15 @@
   user_id: 11
   repo_id: 1
   mode: 3 # auto
+
+-
+  id: 6
+  user_id: 10
+  repo_id: 21
+  mode: 1 # normal
+
+-
+  id: 7
+  user_id: 10
+  repo_id: 32
+  mode: 1 # normal
diff --git a/models/git/branch.go b/models/git/branch.go
index db02fc9b28..2979dff3d2 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -158,6 +158,11 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
 	return &branch, nil
 }
 
+func GetBranches(ctx context.Context, repoID int64, branchNames []string) ([]*Branch, error) {
+	branches := make([]*Branch, 0, len(branchNames))
+	return branches, db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames).Find(&branches)
+}
+
 func AddBranches(ctx context.Context, branches []*Branch) error {
 	for _, branch := range branches {
 		if _, err := db.GetEngine(ctx).Insert(branch); err != nil {
@@ -292,6 +297,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
 
 	sess := db.GetEngine(ctx)
 
+	// check whether from branch exist
 	var branch Branch
 	exist, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, from).Get(&branch)
 	if err != nil {
@@ -303,6 +309,24 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
 		}
 	}
 
+	// check whether to branch exist or is_deleted
+	var dstBranch Branch
+	exist, err = db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, to).Get(&dstBranch)
+	if err != nil {
+		return err
+	}
+	if exist {
+		if !dstBranch.IsDeleted {
+			return ErrBranchAlreadyExists{
+				BranchName: to,
+			}
+		}
+
+		if _, err := db.GetEngine(ctx).ID(dstBranch.ID).NoAutoCondition().Delete(&dstBranch); err != nil {
+			return err
+		}
+	}
+
 	// 1. update branch in database
 	if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{
 		Name: to,
@@ -357,12 +381,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
 		return err
 	}
 
-	// 5. do git action
-	if err = gitAction(ctx, isDefault); err != nil {
-		return err
-	}
-
-	// 6. insert renamed branch record
+	// 5. insert renamed branch record
 	renamedBranch := &RenamedBranch{
 		RepoID: repo.ID,
 		From:   from,
@@ -373,6 +392,11 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
 		return err
 	}
 
+	// 6. do git action
+	if err = gitAction(ctx, isDefault); err != nil {
+		return err
+	}
+
 	return committer.Commit()
 }
 
diff --git a/models/git/branch_list.go b/models/git/branch_list.go
index 0e8d28038a..980bd7b4c9 100644
--- a/models/git/branch_list.go
+++ b/models/git/branch_list.go
@@ -9,7 +9,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"xorm.io/builder"
 )
@@ -17,15 +17,12 @@ import (
 type BranchList []*Branch
 
 func (branches BranchList) LoadDeletedBy(ctx context.Context) error {
-	ids := container.Set[int64]{}
-	for _, branch := range branches {
-		if !branch.IsDeleted {
-			continue
-		}
-		ids.Add(branch.DeletedByID)
-	}
+	ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) {
+		return branch.DeletedByID, branch.IsDeleted
+	})
+
 	usersMap := make(map[int64]*user_model.User, len(ids))
-	if err := db.GetEngine(ctx).In("id", ids.Values()).Find(&usersMap); err != nil {
+	if err := db.GetEngine(ctx).In("id", ids).Find(&usersMap); err != nil {
 		return err
 	}
 	for _, branch := range branches {
@@ -41,14 +38,13 @@ func (branches BranchList) LoadDeletedBy(ctx context.Context) error {
 }
 
 func (branches BranchList) LoadPusher(ctx context.Context) error {
-	ids := container.Set[int64]{}
-	for _, branch := range branches {
-		if branch.PusherID > 0 { // pusher_id maybe zero because some branches are sync by backend with no pusher
-			ids.Add(branch.PusherID)
-		}
-	}
+	ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) {
+		// pusher_id maybe zero because some branches are sync by backend with no pusher
+		return branch.PusherID, branch.PusherID > 0
+	})
+
 	usersMap := make(map[int64]*user_model.User, len(ids))
-	if err := db.GetEngine(ctx).In("id", ids.Values()).Find(&usersMap); err != nil {
+	if err := db.GetEngine(ctx).In("id", ids).Find(&usersMap); err != nil {
 		return err
 	}
 	for _, branch := range branches {
@@ -67,7 +63,7 @@ type FindBranchOptions struct {
 	db.ListOptions
 	RepoID             int64
 	ExcludeBranchNames []string
-	IsDeletedBranch    util.OptionalBool
+	IsDeletedBranch    optional.Option[bool]
 	OrderBy            string
 	Keyword            string
 }
@@ -81,8 +77,8 @@ func (opts FindBranchOptions) ToConds() builder.Cond {
 	if len(opts.ExcludeBranchNames) > 0 {
 		cond = cond.And(builder.NotIn("name", opts.ExcludeBranchNames))
 	}
-	if !opts.IsDeletedBranch.IsNone() {
-		cond = cond.And(builder.Eq{"is_deleted": opts.IsDeletedBranch.IsTrue()})
+	if opts.IsDeletedBranch.Has() {
+		cond = cond.And(builder.Eq{"is_deleted": opts.IsDeletedBranch.Value()})
 	}
 	if opts.Keyword != "" {
 		cond = cond.And(builder.Like{"name", opts.Keyword})
@@ -92,7 +88,7 @@ func (opts FindBranchOptions) ToConds() builder.Cond {
 
 func (opts FindBranchOptions) ToOrders() string {
 	orderBy := opts.OrderBy
-	if !opts.IsDeletedBranch.IsFalse() { // if deleted branch included, put them at the end
+	if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the end
 		if orderBy != "" {
 			orderBy += ", "
 		}
diff --git a/models/git/branch_test.go b/models/git/branch_test.go
index fd5d6519e9..b8ea663e81 100644
--- a/models/git/branch_test.go
+++ b/models/git/branch_test.go
@@ -13,7 +13,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -50,7 +50,7 @@ func TestGetDeletedBranches(t *testing.T) {
 	branches, err := db.Find[git_model.Branch](db.DefaultContext, git_model.FindBranchOptions{
 		ListOptions:     db.ListOptionsAll,
 		RepoID:          repo.ID,
-		IsDeletedBranch: util.OptionalBoolTrue,
+		IsDeletedBranch: optional.Some(true),
 	})
 	assert.NoError(t, err)
 	assert.Len(t, branches, 2)
diff --git a/models/git/commit_status.go b/models/git/commit_status.go
index 5b8b323a34..0ed11cd217 100644
--- a/models/git/commit_status.go
+++ b/models/git/commit_status.go
@@ -26,6 +26,7 @@ import (
 	"code.gitea.io/gitea/modules/translation"
 
 	"xorm.io/builder"
+	"xorm.io/xorm"
 )
 
 // CommitStatus holds a single Status of a single Commit
@@ -195,7 +196,7 @@ func (status *CommitStatus) APIURL(ctx context.Context) string {
 
 // LocaleString returns the locale string name of the Status
 func (status *CommitStatus) LocaleString(lang translation.Locale) string {
-	return lang.Tr("repo.commitstatus." + status.State.String())
+	return lang.TrString("repo.commitstatus." + status.State.String())
 }
 
 // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc
@@ -270,63 +271,70 @@ type CommitStatusIndex struct {
 
 // GetLatestCommitStatus returns all statuses with a unique context for a given commit.
 func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOptions db.ListOptions) ([]*CommitStatus, int64, error) {
-	ids := make([]int64, 0, 10)
-	sess := db.GetEngine(ctx).Table(&CommitStatus{}).
-		Where("repo_id = ?", repoID).And("sha = ?", sha).
-		Select("max( id ) as id").
-		GroupBy("context_hash").OrderBy("max( id ) desc")
+	getBase := func() *xorm.Session {
+		return db.GetEngine(ctx).Table(&CommitStatus{}).
+			Where("repo_id = ?", repoID).And("sha = ?", sha)
+	}
+	indices := make([]int64, 0, 10)
+	sess := getBase().Select("max( `index` ) as `index`").
+		GroupBy("context_hash").OrderBy("max( `index` ) desc")
 	if !listOptions.IsListAll() {
 		sess = db.SetSessionPagination(sess, &listOptions)
 	}
-	count, err := sess.FindAndCount(&ids)
+	count, err := sess.FindAndCount(&indices)
 	if err != nil {
 		return nil, count, err
 	}
-	statuses := make([]*CommitStatus, 0, len(ids))
-	if len(ids) == 0 {
+	statuses := make([]*CommitStatus, 0, len(indices))
+	if len(indices) == 0 {
 		return statuses, count, nil
 	}
-	return statuses, count, db.GetEngine(ctx).In("id", ids).Find(&statuses)
+	return statuses, count, getBase().And(builder.In("`index`", indices)).Find(&statuses)
 }
 
 // GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs
-func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHAs map[int64]string, listOptions db.ListOptions) (map[int64][]*CommitStatus, error) {
+func GetLatestCommitStatusForPairs(ctx context.Context, repoSHAs []RepoSHA) (map[int64][]*CommitStatus, error) {
 	type result struct {
-		ID     int64
+		Index  int64
 		RepoID int64
+		SHA    string
 	}
 
-	results := make([]result, 0, len(repoIDsToLatestCommitSHAs))
+	results := make([]result, 0, len(repoSHAs))
 
-	sess := db.GetEngine(ctx).Table(&CommitStatus{})
+	getBase := func() *xorm.Session {
+		return db.GetEngine(ctx).Table(&CommitStatus{})
+	}
 
 	// Create a disjunction of conditions for each repoID and SHA pair
-	conds := make([]builder.Cond, 0, len(repoIDsToLatestCommitSHAs))
-	for repoID, sha := range repoIDsToLatestCommitSHAs {
-		conds = append(conds, builder.Eq{"repo_id": repoID, "sha": sha})
-	}
-	sess = sess.Where(builder.Or(conds...)).
-		Select("max( id ) as id, repo_id").
-		GroupBy("context_hash, repo_id").OrderBy("max( id ) desc")
-
-	if !listOptions.IsListAll() {
-		sess = db.SetSessionPagination(sess, &listOptions)
+	conds := make([]builder.Cond, 0, len(repoSHAs))
+	for _, repoSHA := range repoSHAs {
+		conds = append(conds, builder.Eq{"repo_id": repoSHA.RepoID, "sha": repoSHA.SHA})
 	}
+	sess := getBase().Where(builder.Or(conds...)).
+		Select("max( `index` ) as `index`, repo_id, sha").
+		GroupBy("context_hash, repo_id, sha").OrderBy("max( `index` ) desc")
 
 	err := sess.Find(&results)
 	if err != nil {
 		return nil, err
 	}
 
-	ids := make([]int64, 0, len(results))
 	repoStatuses := make(map[int64][]*CommitStatus)
-	for _, result := range results {
-		ids = append(ids, result.ID)
-	}
 
-	statuses := make([]*CommitStatus, 0, len(ids))
-	if len(ids) > 0 {
-		err = db.GetEngine(ctx).In("id", ids).Find(&statuses)
+	if len(results) > 0 {
+		statuses := make([]*CommitStatus, 0, len(results))
+
+		conds = make([]builder.Cond, 0, len(results))
+		for _, result := range results {
+			cond := builder.Eq{
+				"`index`": result.Index,
+				"repo_id": result.RepoID,
+				"sha":     result.SHA,
+			}
+			conds = append(conds, cond)
+		}
+		err = getBase().Where(builder.Or(conds...)).Find(&statuses)
 		if err != nil {
 			return nil, err
 		}
@@ -343,42 +351,43 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHA
 // GetLatestCommitStatusForRepoCommitIDs returns all statuses with a unique context for a given list of repo-sha pairs
 func GetLatestCommitStatusForRepoCommitIDs(ctx context.Context, repoID int64, commitIDs []string) (map[string][]*CommitStatus, error) {
 	type result struct {
-		ID  int64
-		Sha string
+		Index int64
+		SHA   string
 	}
 
+	getBase := func() *xorm.Session {
+		return db.GetEngine(ctx).Table(&CommitStatus{}).Where("repo_id = ?", repoID)
+	}
 	results := make([]result, 0, len(commitIDs))
 
-	sess := db.GetEngine(ctx).Table(&CommitStatus{})
-
-	// Create a disjunction of conditions for each repoID and SHA pair
 	conds := make([]builder.Cond, 0, len(commitIDs))
 	for _, sha := range commitIDs {
 		conds = append(conds, builder.Eq{"sha": sha})
 	}
-	sess = sess.Where(builder.Eq{"repo_id": repoID}.And(builder.Or(conds...))).
-		Select("max( id ) as id, sha").
-		GroupBy("context_hash, sha").OrderBy("max( id ) desc")
+	sess := getBase().And(builder.Or(conds...)).
+		Select("max( `index` ) as `index`, sha").
+		GroupBy("context_hash, sha").OrderBy("max( `index` ) desc")
 
 	err := sess.Find(&results)
 	if err != nil {
 		return nil, err
 	}
 
-	ids := make([]int64, 0, len(results))
 	repoStatuses := make(map[string][]*CommitStatus)
-	for _, result := range results {
-		ids = append(ids, result.ID)
-	}
 
-	statuses := make([]*CommitStatus, 0, len(ids))
-	if len(ids) > 0 {
-		err = db.GetEngine(ctx).In("id", ids).Find(&statuses)
+	if len(results) > 0 {
+		statuses := make([]*CommitStatus, 0, len(results))
+
+		conds = make([]builder.Cond, 0, len(results))
+		for _, result := range results {
+			conds = append(conds, builder.Eq{"`index`": result.Index, "sha": result.SHA})
+		}
+		err = getBase().And(builder.Or(conds...)).Find(&statuses)
 		if err != nil {
 			return nil, err
 		}
 
-		// Group the statuses by repo ID
+		// Group the statuses by commit
 		for _, status := range statuses {
 			repoStatuses[status.SHA] = append(repoStatuses[status.SHA], status)
 		}
@@ -389,22 +398,36 @@ func GetLatestCommitStatusForRepoCommitIDs(ctx context.Context, repoID int64, co
 
 // FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts
 func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, before time.Duration) ([]string, error) {
+	type result struct {
+		Index int64
+		SHA   string
+	}
+	getBase := func() *xorm.Session {
+		return db.GetEngine(ctx).Table(&CommitStatus{}).Where("repo_id = ?", repoID)
+	}
+
 	start := timeutil.TimeStampNow().AddDuration(-before)
-	ids := make([]int64, 0, 10)
-	if err := db.GetEngine(ctx).Table("commit_status").
-		Where("repo_id = ?", repoID).
-		And("updated_unix >= ?", start).
-		Select("max( id ) as id").
-		GroupBy("context_hash").OrderBy("max( id ) desc").
-		Find(&ids); err != nil {
+	results := make([]result, 0, 10)
+
+	sess := getBase().And("updated_unix >= ?", start).
+		Select("max( `index` ) as `index`, sha").
+		GroupBy("context_hash, sha").OrderBy("max( `index` ) desc")
+
+	err := sess.Find(&results)
+	if err != nil {
 		return nil, err
 	}
 
-	contexts := make([]string, 0, len(ids))
-	if len(ids) == 0 {
+	contexts := make([]string, 0, len(results))
+	if len(results) == 0 {
 		return contexts, nil
 	}
-	return contexts, db.GetEngine(ctx).Select("context").Table("commit_status").In("id", ids).Find(&contexts)
+
+	conds := make([]builder.Cond, 0, len(results))
+	for _, result := range results {
+		conds = append(conds, builder.Eq{"`index`": result.Index, "sha": result.SHA})
+	}
+	return contexts, getBase().And(builder.Or(conds...)).Select("context").Find(&contexts)
 }
 
 // NewCommitStatusOptions holds options for creating a CommitStatus
diff --git a/models/git/commit_status_summary.go b/models/git/commit_status_summary.go
new file mode 100644
index 0000000000..01674e943d
--- /dev/null
+++ b/models/git/commit_status_summary.go
@@ -0,0 +1,84 @@
+// Copyright 2024 Gitea. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/setting"
+	api "code.gitea.io/gitea/modules/structs"
+
+	"xorm.io/builder"
+)
+
+// CommitStatusSummary holds the latest commit Status of a single Commit
+type CommitStatusSummary struct {
+	ID     int64                 `xorm:"pk autoincr"`
+	RepoID int64                 `xorm:"INDEX UNIQUE(repo_id_sha)"`
+	SHA    string                `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"`
+	State  api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
+}
+
+func init() {
+	db.RegisterModel(new(CommitStatusSummary))
+}
+
+type RepoSHA struct {
+	RepoID int64
+	SHA    string
+}
+
+func GetLatestCommitStatusForRepoAndSHAs(ctx context.Context, repoSHAs []RepoSHA) ([]*CommitStatus, error) {
+	cond := builder.NewCond()
+	for _, rs := range repoSHAs {
+		cond = cond.Or(builder.Eq{"repo_id": rs.RepoID, "sha": rs.SHA})
+	}
+
+	var summaries []CommitStatusSummary
+	if err := db.GetEngine(ctx).Where(cond).Find(&summaries); err != nil {
+		return nil, err
+	}
+
+	commitStatuses := make([]*CommitStatus, 0, len(repoSHAs))
+	for _, summary := range summaries {
+		commitStatuses = append(commitStatuses, &CommitStatus{
+			RepoID: summary.RepoID,
+			SHA:    summary.SHA,
+			State:  summary.State,
+		})
+	}
+	return commitStatuses, nil
+}
+
+func UpdateCommitStatusSummary(ctx context.Context, repoID int64, sha string) error {
+	commitStatuses, _, err := GetLatestCommitStatus(ctx, repoID, sha, db.ListOptionsAll)
+	if err != nil {
+		return err
+	}
+	state := CalcCommitStatus(commitStatuses)
+	// mysql will return 0 when update a record which state hasn't been changed which behaviour is different from other database,
+	// so we need to use insert in on duplicate
+	if setting.Database.Type.IsMySQL() {
+		_, err := db.GetEngine(ctx).Exec("INSERT INTO commit_status_summary (repo_id,sha,state) VALUES (?,?,?) ON DUPLICATE KEY UPDATE state=?",
+			repoID, sha, state.State, state.State)
+		return err
+	}
+
+	if cnt, err := db.GetEngine(ctx).Where("repo_id=? AND sha=?", repoID, sha).
+		Cols("state").
+		Update(&CommitStatusSummary{
+			State: state.State,
+		}); err != nil {
+		return err
+	} else if cnt == 0 {
+		_, err = db.GetEngine(ctx).Insert(&CommitStatusSummary{
+			RepoID: repoID,
+			SHA:    sha,
+			State:  state.State,
+		})
+		return err
+	}
+	return nil
+}
diff --git a/models/git/protected_branch_list.go b/models/git/protected_branch_list.go
index eeb307e245..613333a5a2 100644
--- a/models/git/protected_branch_list.go
+++ b/models/git/protected_branch_list.go
@@ -8,7 +8,7 @@ import (
 	"sort"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/gobwas/glob"
 )
@@ -56,7 +56,7 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string)
 				Page:     page,
 			},
 			RepoID:          repoID,
-			IsDeletedBranch: util.OptionalBoolFalse,
+			IsDeletedBranch: optional.Some(false),
 		})
 		if err != nil {
 			return nil, err
diff --git a/models/issues/assignees.go b/models/issues/assignees.go
index 60f32d9557..30234be07a 100644
--- a/models/issues/assignees.go
+++ b/models/issues/assignees.go
@@ -64,6 +64,27 @@ func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.U
 	return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID})
 }
 
+type AssignedIssuesOptions struct {
+	db.ListOptions
+	AssigneeID  int64
+	RepoOwnerID int64
+}
+
+func (opts *AssignedIssuesOptions) ToConds() builder.Cond {
+	cond := builder.NewCond()
+	if opts.AssigneeID != 0 {
+		cond = cond.And(builder.In("issue.id", builder.Select("issue_id").From("issue_assignees").Where(builder.Eq{"assignee_id": opts.AssigneeID})))
+	}
+	if opts.RepoOwnerID != 0 {
+		cond = cond.And(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": opts.RepoOwnerID})))
+	}
+	return cond
+}
+
+func GetAssignedIssues(ctx context.Context, opts *AssignedIssuesOptions) ([]*Issue, int64, error) {
+	return db.FindAndCount[Issue](ctx, opts)
+}
+
 // ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
 func ToggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) {
 	ctx, committer, err := db.TxContext(ctx)
diff --git a/models/issues/comment.go b/models/issues/comment.go
index c63fcab894..353163ebd6 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -8,6 +8,7 @@ package issues
 import (
 	"context"
 	"fmt"
+	"html/template"
 	"strconv"
 	"unicode/utf8"
 
@@ -21,6 +22,7 @@ import (
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/references"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -210,12 +212,12 @@ const (
 
 // LocaleString returns the locale string name of the role
 func (r RoleInRepo) LocaleString(lang translation.Locale) string {
-	return lang.Tr("repo.issues.role." + string(r))
+	return lang.TrString("repo.issues.role." + string(r))
 }
 
 // LocaleHelper returns the locale tooltip of the role
 func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
-	return lang.Tr("repo.issues.role." + string(r) + "_helper")
+	return lang.TrString("repo.issues.role." + string(r) + "_helper")
 }
 
 // Comment represents a comment in commit and issue page.
@@ -259,8 +261,8 @@ type Comment struct {
 	CommitID        int64
 	Line            int64 // - previous line / + proposed line
 	TreePath        string
-	Content         string `xorm:"LONGTEXT"`
-	RenderedContent string `xorm:"-"`
+	Content         string        `xorm:"LONGTEXT"`
+	RenderedContent template.HTML `xorm:"-"`
 
 	// Path represents the 4 lines of code cemented by this comment
 	Patch       string `xorm:"-"`
@@ -671,7 +673,8 @@ func (c *Comment) LoadTime(ctx context.Context) error {
 	return err
 }
 
-func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository) (err error) {
+// LoadReactions loads comment reactions
+func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository) (err error) {
 	if c.Reactions != nil {
 		return nil
 	}
@@ -689,14 +692,16 @@ func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository
 	return nil
 }
 
-// LoadReactions loads comment reactions
-func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository) error {
-	return c.loadReactions(ctx, repo)
-}
-
 func (c *Comment) loadReview(ctx context.Context) (err error) {
+	if c.ReviewID == 0 {
+		return nil
+	}
 	if c.Review == nil {
 		if c.Review, err = GetReviewByID(ctx, c.ReviewID); err != nil {
+			// review request which has been replaced by actual reviews doesn't exist in database anymore, so ignorem them.
+			if c.Type == CommentTypeReviewRequest {
+				return nil
+			}
 			return err
 		}
 	}
@@ -848,6 +853,9 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
 	// Check comment type.
 	switch opts.Type {
 	case CommentTypeCode:
+		if err = updateAttachments(ctx, opts, comment); err != nil {
+			return err
+		}
 		if comment.ReviewID != 0 {
 			if comment.Review == nil {
 				if err := comment.loadReview(ctx); err != nil {
@@ -865,22 +873,9 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
 		}
 		fallthrough
 	case CommentTypeReview:
-		// Check attachments
-		attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
-		if err != nil {
-			return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
+		if err = updateAttachments(ctx, opts, comment); err != nil {
+			return err
 		}
-
-		for i := range attachments {
-			attachments[i].IssueID = opts.Issue.ID
-			attachments[i].CommentID = comment.ID
-			// No assign value could be 0, so ignore AllCols().
-			if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil {
-				return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err)
-			}
-		}
-
-		comment.Attachments = attachments
 	case CommentTypeReopen, CommentTypeClose:
 		if err = repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.Issue.IsPull, true); err != nil {
 			return err
@@ -890,6 +885,23 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
 	return UpdateIssueCols(ctx, opts.Issue, "updated_unix")
 }
 
+func updateAttachments(ctx context.Context, opts *CreateCommentOptions, comment *Comment) error {
+	attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
+	if err != nil {
+		return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
+	}
+	for i := range attachments {
+		attachments[i].IssueID = opts.Issue.ID
+		attachments[i].CommentID = comment.ID
+		// No assign value could be 0, so ignore AllCols().
+		if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil {
+			return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err)
+		}
+	}
+	comment.Attachments = attachments
+	return nil
+}
+
 func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) {
 	var content string
 	var commentType CommentType
@@ -1021,8 +1033,8 @@ type FindCommentsOptions struct {
 	TreePath    string
 	Type        CommentType
 	IssueIDs    []int64
-	Invalidated util.OptionalBool
-	IsPull      util.OptionalBool
+	Invalidated optional.Option[bool]
+	IsPull      optional.Option[bool]
 }
 
 // ToConds implements FindOptions interface
@@ -1054,11 +1066,11 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
 	if len(opts.TreePath) > 0 {
 		cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath})
 	}
-	if !opts.Invalidated.IsNone() {
-		cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()})
+	if opts.Invalidated.Has() {
+		cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.Value()})
 	}
-	if opts.IsPull != util.OptionalBoolNone {
-		cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()})
+	if opts.IsPull.Has() {
+		cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.Value()})
 	}
 	return cond
 }
@@ -1067,7 +1079,7 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
 func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
 	comments := make([]*Comment, 0, 10)
 	sess := db.GetEngine(ctx).Where(opts.ToConds())
-	if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone {
+	if opts.RepoID > 0 || opts.IsPull.Has() {
 		sess.Join("INNER", "issue", "issue.id = comment.issue_id")
 	}
 
@@ -1260,10 +1272,9 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error {
 		return nil
 	}
 
-	issueIDs := make(container.Set[int64])
-	for _, comment := range comments {
-		issueIDs.Add(comment.IssueID)
-	}
+	issueIDs := container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.IssueID, true
+	})
 
 	ctx, committer, err := db.TxContext(ctx)
 	if err != nil {
@@ -1286,7 +1297,7 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error {
 		}
 	}
 
-	for issueID := range issueIDs {
+	for _, issueID := range issueIDs {
 		if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?",
 			issueID, CommentTypeComment, issueID); err != nil {
 			return err
diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go
index 384a595dd9..f860dacfac 100644
--- a/models/issues/comment_code.go
+++ b/models/issues/comment_code.go
@@ -74,6 +74,10 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
 		return nil, err
 	}
 
+	if err := comments.LoadAttachments(ctx); err != nil {
+		return nil, err
+	}
+
 	// Find all reviews by ReviewID
 	reviews := make(map[int64]*Review)
 	ids := make([]int64, 0, len(comments))
@@ -122,7 +126,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
 }
 
 // FetchCodeCommentsByLine fetches the code comments for a given treePath and line number
-func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) ([]*Comment, error) {
+func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) (CommentList, error) {
 	opts := FindCommentsOptions{
 		Type:     CommentTypeCode,
 		IssueID:  issue.ID,
diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go
index 93af45870e..370b5396e0 100644
--- a/models/issues/comment_list.go
+++ b/models/issues/comment_list.go
@@ -17,11 +17,9 @@ import (
 type CommentList []*Comment
 
 func (comments CommentList) getPosterIDs() []int64 {
-	posterIDs := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		posterIDs.Add(comment.PosterID)
-	}
-	return posterIDs.Values()
+	return container.FilterSlice(comments, func(c *Comment) (int64, bool) {
+		return c.PosterID, c.PosterID > 0
+	})
 }
 
 // LoadPosters loads posters
@@ -41,20 +39,10 @@ func (comments CommentList) LoadPosters(ctx context.Context) error {
 	return nil
 }
 
-func (comments CommentList) getCommentIDs() []int64 {
-	ids := make([]int64, 0, len(comments))
-	for _, comment := range comments {
-		ids = append(ids, comment.ID)
-	}
-	return ids
-}
-
 func (comments CommentList) getLabelIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		ids.Add(comment.LabelID)
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.LabelID, comment.LabelID > 0
+	})
 }
 
 func (comments CommentList) loadLabels(ctx context.Context) error {
@@ -98,11 +86,9 @@ func (comments CommentList) loadLabels(ctx context.Context) error {
 }
 
 func (comments CommentList) getMilestoneIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		ids.Add(comment.MilestoneID)
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.MilestoneID, comment.MilestoneID > 0
+	})
 }
 
 func (comments CommentList) loadMilestones(ctx context.Context) error {
@@ -139,11 +125,9 @@ func (comments CommentList) loadMilestones(ctx context.Context) error {
 }
 
 func (comments CommentList) getOldMilestoneIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		ids.Add(comment.OldMilestoneID)
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.OldMilestoneID, comment.OldMilestoneID > 0
+	})
 }
 
 func (comments CommentList) loadOldMilestones(ctx context.Context) error {
@@ -180,11 +164,9 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error {
 }
 
 func (comments CommentList) getAssigneeIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		ids.Add(comment.AssigneeID)
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.AssigneeID, comment.AssigneeID > 0
+	})
 }
 
 func (comments CommentList) loadAssignees(ctx context.Context) error {
@@ -225,20 +207,19 @@ func (comments CommentList) loadAssignees(ctx context.Context) error {
 
 	for _, comment := range comments {
 		comment.Assignee = assignees[comment.AssigneeID]
+		if comment.Assignee == nil {
+			comment.AssigneeID = user_model.GhostUserID
+			comment.Assignee = user_model.NewGhostUser()
+		}
 	}
 	return nil
 }
 
 // getIssueIDs returns all the issue ids on this comment list which issue hasn't been loaded
 func (comments CommentList) getIssueIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		if comment.Issue != nil {
-			continue
-		}
-		ids.Add(comment.IssueID)
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.IssueID, comment.Issue == nil
+	})
 }
 
 // Issues returns all the issues of comments
@@ -305,14 +286,12 @@ func (comments CommentList) LoadIssues(ctx context.Context) error {
 }
 
 func (comments CommentList) getDependentIssueIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
 		if comment.DependentIssue != nil {
-			continue
+			return 0, false
 		}
-		ids.Add(comment.DependentIssueID)
-	}
-	return ids.Values()
+		return comment.DependentIssueID, comment.DependentIssueID > 0
+	})
 }
 
 func (comments CommentList) loadDependentIssues(ctx context.Context) error {
@@ -365,6 +344,35 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error {
 	return nil
 }
 
+// getAttachmentCommentIDs only return the comment ids which possibly has attachments
+func (comments CommentList) getAttachmentCommentIDs() []int64 {
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.ID, comment.Type.HasAttachmentSupport()
+	})
+}
+
+// LoadAttachmentsByIssue loads attachments by issue id
+func (comments CommentList) LoadAttachmentsByIssue(ctx context.Context) error {
+	if len(comments) == 0 {
+		return nil
+	}
+
+	attachments := make([]*repo_model.Attachment, 0, len(comments)/2)
+	if err := db.GetEngine(ctx).Where("issue_id=? AND comment_id>0", comments[0].IssueID).Find(&attachments); err != nil {
+		return err
+	}
+
+	commentAttachmentsMap := make(map[int64][]*repo_model.Attachment, len(comments))
+	for _, attach := range attachments {
+		commentAttachmentsMap[attach.CommentID] = append(commentAttachmentsMap[attach.CommentID], attach)
+	}
+
+	for _, comment := range comments {
+		comment.Attachments = commentAttachmentsMap[comment.ID]
+	}
+	return nil
+}
+
 // LoadAttachments loads attachments
 func (comments CommentList) LoadAttachments(ctx context.Context) (err error) {
 	if len(comments) == 0 {
@@ -372,16 +380,15 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) {
 	}
 
 	attachments := make(map[int64][]*repo_model.Attachment, len(comments))
-	commentsIDs := comments.getCommentIDs()
+	commentsIDs := comments.getAttachmentCommentIDs()
 	left := len(commentsIDs)
 	for left > 0 {
 		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
-		rows, err := db.GetEngine(ctx).Table("attachment").
-			Join("INNER", "comment", "comment.id = attachment.comment_id").
-			In("comment.id", commentsIDs[:limit]).
+		rows, err := db.GetEngine(ctx).
+			In("comment_id", commentsIDs[:limit]).
 			Rows(new(repo_model.Attachment))
 		if err != nil {
 			return err
@@ -409,11 +416,9 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) {
 }
 
 func (comments CommentList) getReviewIDs() []int64 {
-	ids := make(container.Set[int64], len(comments))
-	for _, comment := range comments {
-		ids.Add(comment.ReviewID)
-	}
-	return ids.Values()
+	return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
+		return comment.ReviewID, comment.ReviewID > 0
+	})
 }
 
 func (comments CommentList) loadReviews(ctx context.Context) error {
@@ -430,7 +435,8 @@ func (comments CommentList) loadReviews(ctx context.Context) error {
 	for _, comment := range comments {
 		comment.Review = reviews[comment.ReviewID]
 		if comment.Review == nil {
-			if comment.ReviewID > 0 {
+			// review request which has been replaced by actual reviews doesn't exist in database anymore, so don't log errors for them.
+			if comment.ReviewID > 0 && comment.Type != CommentTypeReviewRequest {
 				log.Error("comment with review id [%d] but has no review record", comment.ReviewID)
 			}
 			continue
diff --git a/models/issues/content_history.go b/models/issues/content_history.go
index 8c333bc6dd..31c80d2cea 100644
--- a/models/issues/content_history.go
+++ b/models/issues/content_history.go
@@ -161,20 +161,20 @@ func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int6
 	}
 
 	for _, item := range res {
-		item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0)
+		if item.UserID > 0 {
+			item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0)
+		} else {
+			item.UserAvatarLink = avatars.DefaultAvatarLink()
+		}
 	}
 	return res, nil
 }
 
 // HasIssueContentHistory check if a ContentHistory entry exists
 func HasIssueContentHistory(dbCtx context.Context, issueID, commentID int64) (bool, error) {
-	exists, err := db.GetEngine(dbCtx).Cols("id").Exist(&ContentHistory{
-		IssueID:   issueID,
-		CommentID: commentID,
-	})
+	exists, err := db.GetEngine(dbCtx).Where(builder.Eq{"issue_id": issueID, "comment_id": commentID}).Exist(&ContentHistory{})
 	if err != nil {
-		log.Error("can not fetch issue content history. err=%v", err)
-		return false, err
+		return false, fmt.Errorf("can not check issue content history. err: %w", err)
 	}
 	return exists, err
 }
diff --git a/models/issues/content_history_test.go b/models/issues/content_history_test.go
index 0ea1d0f7b2..1caa73a948 100644
--- a/models/issues/content_history_test.go
+++ b/models/issues/content_history_test.go
@@ -78,3 +78,22 @@ func TestContentHistory(t *testing.T) {
 	assert.EqualValues(t, 7, list2[1].HistoryID)
 	assert.EqualValues(t, 4, list2[2].HistoryID)
 }
+
+func TestHasIssueContentHistoryForCommentOnly(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	_ = db.TruncateBeans(db.DefaultContext, &issues_model.ContentHistory{})
+
+	hasHistory1, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0)
+	assert.False(t, hasHistory1)
+	hasHistory2, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100)
+	assert.False(t, hasHistory2)
+
+	_ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow(), "c-a", true)
+	_ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow().Add(5), "c-b", false)
+
+	hasHistory1, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0)
+	assert.False(t, hasHistory1)
+	hasHistory2, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100)
+	assert.True(t, hasHistory2)
+}
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 90aad10bb9..87c1c86eb1 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -7,6 +7,7 @@ package issues
 import (
 	"context"
 	"fmt"
+	"html/template"
 	"regexp"
 	"slices"
 
@@ -105,7 +106,7 @@ type Issue struct {
 	OriginalAuthorID int64                  `xorm:"index"`
 	Title            string                 `xorm:"name"`
 	Content          string                 `xorm:"LONGTEXT"`
-	RenderedContent  string                 `xorm:"-"`
+	RenderedContent  template.HTML          `xorm:"-"`
 	Labels           []*Label               `xorm:"-"`
 	MilestoneID      int64                  `xorm:"INDEX"`
 	Milestone        *Milestone             `xorm:"-"`
@@ -192,20 +193,6 @@ func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
 	return issue.Repo.IsTimetrackerEnabled(ctx)
 }
 
-// GetPullRequest returns the issue pull request
-func (issue *Issue) GetPullRequest(ctx context.Context) (pr *PullRequest, err error) {
-	if !issue.IsPull {
-		return nil, fmt.Errorf("Issue is not a pull request")
-	}
-
-	pr, err = GetPullRequestByIssueID(ctx, issue.ID)
-	if err != nil {
-		return nil, err
-	}
-	pr.Issue = issue
-	return pr, err
-}
-
 // LoadPoster loads poster
 func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
 	if issue.Poster == nil && issue.PosterID != 0 {
diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go
index a932ac2554..f8ee271a6b 100644
--- a/models/issues/issue_list.go
+++ b/models/issues/issue_list.go
@@ -21,16 +21,15 @@ type IssueList []*Issue
 
 // get the repo IDs to be loaded later, these IDs are for issue.Repo and issue.PullRequest.HeadRepo
 func (issues IssueList) getRepoIDs() []int64 {
-	repoIDs := make(container.Set[int64], len(issues))
-	for _, issue := range issues {
+	return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
 		if issue.Repo == nil {
-			repoIDs.Add(issue.RepoID)
+			return issue.RepoID, true
 		}
 		if issue.PullRequest != nil && issue.PullRequest.HeadRepo == nil {
-			repoIDs.Add(issue.PullRequest.HeadRepoID)
+			return issue.PullRequest.HeadRepoID, true
 		}
-	}
-	return repoIDs.Values()
+		return 0, false
+	})
 }
 
 // LoadRepositories loads issues' all repositories
@@ -74,11 +73,9 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi
 }
 
 func (issues IssueList) getPosterIDs() []int64 {
-	posterIDs := make(container.Set[int64], len(issues))
-	for _, issue := range issues {
-		posterIDs.Add(issue.PosterID)
-	}
-	return posterIDs.Values()
+	return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
+		return issue.PosterID, true
+	})
 }
 
 func (issues IssueList) loadPosters(ctx context.Context) error {
@@ -193,11 +190,9 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
 }
 
 func (issues IssueList) getMilestoneIDs() []int64 {
-	ids := make(container.Set[int64], len(issues))
-	for _, issue := range issues {
-		ids.Add(issue.MilestoneID)
-	}
-	return ids.Values()
+	return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
+		return issue.MilestoneID, true
+	})
 }
 
 func (issues IssueList) loadMilestones(ctx context.Context) error {
@@ -370,6 +365,9 @@ func (issues IssueList) LoadPullRequests(ctx context.Context) error {
 
 	for _, issue := range issues {
 		issue.PullRequest = pullRequestMaps[issue.ID]
+		if issue.PullRequest != nil {
+			issue.PullRequest.Issue = issue
+		}
 	}
 	return nil
 }
@@ -388,9 +386,8 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
 		if left < limit {
 			limit = left
 		}
-		rows, err := db.GetEngine(ctx).Table("attachment").
-			Join("INNER", "issue", "issue.id = attachment.issue_id").
-			In("issue.id", issuesIDs[:limit]).
+		rows, err := db.GetEngine(ctx).
+			In("issue_id", issuesIDs[:limit]).
 			Rows(new(repo_model.Attachment))
 		if err != nil {
 			return err
@@ -476,6 +473,16 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) {
 	}
 	trackedTimes := make(map[int64]int64, len(issues))
 
+	reposMap := make(map[int64]*repo_model.Repository, len(issues))
+	for _, issue := range issues {
+		reposMap[issue.RepoID] = issue.Repo
+	}
+	repos := repo_model.RepositoryListOfMap(reposMap)
+
+	if err := repos.LoadUnits(ctx); err != nil {
+		return err
+	}
+
 	ids := make([]int64, 0, len(issues))
 	for _, issue := range issues {
 		if issue.Repo.IsTimetrackerEnabled(ctx) {
@@ -599,3 +606,23 @@ func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*Rev
 
 	return approvalCountMap, nil
 }
+
+func (issues IssueList) LoadIsRead(ctx context.Context, userID int64) error {
+	issueIDs := issues.getIssueIDs()
+	issueUsers := make([]*IssueUser, 0, len(issueIDs))
+	if err := db.GetEngine(ctx).Where("uid =?", userID).
+		In("issue_id").
+		Find(&issueUsers); err != nil {
+		return err
+	}
+
+	for _, issueUser := range issueUsers {
+		for _, issue := range issues {
+			if issue.ID == issueUser.IssueID {
+				issue.IsRead = issueUser.IsRead
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go
index cc7ffb356a..907a5a17b9 100644
--- a/models/issues/issue_project.go
+++ b/models/issues/issue_project.go
@@ -49,18 +49,13 @@ func (issue *Issue) ProjectBoardID(ctx context.Context) int64 {
 
 // LoadIssuesFromBoard load issues assigned to this board
 func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) {
-	issueList := make(IssueList, 0, 10)
-
-	if b.ID > 0 {
-		issues, err := Issues(ctx, &IssuesOptions{
-			ProjectBoardID: b.ID,
-			ProjectID:      b.ProjectID,
-			SortType:       "project-column-sorting",
-		})
-		if err != nil {
-			return nil, err
-		}
-		issueList = issues
+	issueList, err := Issues(ctx, &IssuesOptions{
+		ProjectBoardID: b.ID,
+		ProjectID:      b.ProjectID,
+		SortType:       "project-column-sorting",
+	})
+	if err != nil {
+		return nil, err
 	}
 
 	if b.Default {
diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go
index 7dc277327a..921dd9973e 100644
--- a/models/issues/issue_search.go
+++ b/models/issues/issue_search.go
@@ -13,7 +13,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"xorm.io/builder"
 	"xorm.io/xorm"
@@ -21,7 +21,7 @@ import (
 
 // IssuesOptions represents options of an issue.
 type IssuesOptions struct { //nolint
-	db.Paginator
+	Paginator          *db.ListOptions
 	RepoIDs            []int64 // overwrites RepoCond if the length is not 0
 	AllPublic          bool    // include also all public repositories
 	RepoCond           builder.Cond
@@ -34,8 +34,8 @@ type IssuesOptions struct { //nolint
 	MilestoneIDs       []int64
 	ProjectID          int64
 	ProjectBoardID     int64
-	IsClosed           util.OptionalBool
-	IsPull             util.OptionalBool
+	IsClosed           optional.Option[bool]
+	IsPull             optional.Option[bool]
 	LabelIDs           []int64
 	IncludedLabelNames []string
 	ExcludedLabelNames []string
@@ -46,7 +46,7 @@ type IssuesOptions struct { //nolint
 	UpdatedBeforeUnix  int64
 	// prioritize issues from this repo
 	PriorityRepoID int64
-	IsArchived     util.OptionalBool
+	IsArchived     optional.Option[bool]
 	Org            *organization.Organization // issues permission scope
 	Team           *organization.Team         // issues permission scope
 	User           *user_model.User           // issues permission scope
@@ -104,23 +104,11 @@ func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
 		return sess
 	}
 
-	// Warning: Do not use GetSkipTake() for *db.ListOptions
-	// Its implementation could reset the page size with setting.API.MaxResponseItems
-	if listOptions, ok := opts.Paginator.(*db.ListOptions); ok {
-		if listOptions.Page >= 0 && listOptions.PageSize > 0 {
-			var start int
-			if listOptions.Page == 0 {
-				start = 0
-			} else {
-				start = (listOptions.Page - 1) * listOptions.PageSize
-			}
-			sess.Limit(listOptions.PageSize, start)
-		}
-		return sess
+	start := 0
+	if opts.Paginator.Page > 1 {
+		start = (opts.Paginator.Page - 1) * opts.Paginator.PageSize
 	}
-
-	start, limit := opts.Paginator.GetSkipTake()
-	sess.Limit(limit, start)
+	sess.Limit(opts.Paginator.PageSize, start)
 
 	return sess
 }
@@ -217,8 +205,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
 
 	applyRepoConditions(sess, opts)
 
-	if !opts.IsClosed.IsNone() {
-		sess.And("issue.is_closed=?", opts.IsClosed.IsTrue())
+	if opts.IsClosed.Has() {
+		sess.And("issue.is_closed=?", opts.IsClosed.Value())
 	}
 
 	if opts.AssigneeID > 0 {
@@ -260,21 +248,18 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
 
 	applyProjectBoardCondition(sess, opts)
 
-	switch opts.IsPull {
-	case util.OptionalBoolTrue:
-		sess.And("issue.is_pull=?", true)
-	case util.OptionalBoolFalse:
-		sess.And("issue.is_pull=?", false)
+	if opts.IsPull.Has() {
+		sess.And("issue.is_pull=?", opts.IsPull.Value())
 	}
 
-	if opts.IsArchived != util.OptionalBoolNone {
-		sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()})
+	if opts.IsArchived.Has() {
+		sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.Value()})
 	}
 
 	applyLabelsCondition(sess, opts)
 
 	if opts.User != nil {
-		sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue()))
+		sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()))
 	}
 
 	return sess
@@ -396,7 +381,7 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64)
 
 func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session {
 	// Query for pull requests where you are a reviewer or commenter, excluding
-	// any pull requests already returned by the the review requested filter.
+	// any pull requests already returned by the review requested filter.
 	notPoster := builder.Neq{"issue.poster_id": reviewedID}
 	reviewed := builder.In("issue.id", builder.
 		Select("issue_id").
diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go
index 99ca19f804..39326616f8 100644
--- a/models/issues/issue_stats.go
+++ b/models/issues/issue_stats.go
@@ -8,7 +8,6 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/util"
 
 	"xorm.io/builder"
 	"xorm.io/xorm"
@@ -69,13 +68,17 @@ func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int6
 }
 
 // CountIssues number return of issues by given conditions.
-func CountIssues(ctx context.Context, opts *IssuesOptions) (int64, error) {
+func CountIssues(ctx context.Context, opts *IssuesOptions, otherConds ...builder.Cond) (int64, error) {
 	sess := db.GetEngine(ctx).
 		Select("COUNT(issue.id) AS count").
 		Table("issue").
 		Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
 	applyConditions(sess, opts)
 
+	for _, cond := range otherConds {
+		sess.And(cond)
+	}
+
 	return sess.Count()
 }
 
@@ -170,11 +173,8 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6
 		applyReviewedCondition(sess, opts.ReviewedID)
 	}
 
-	switch opts.IsPull {
-	case util.OptionalBoolTrue:
-		sess.And("issue.is_pull=?", true)
-	case util.OptionalBoolFalse:
-		sess.And("issue.is_pull=?", false)
+	if opts.IsPull.Has() {
+		sess.And("issue.is_pull=?", opts.IsPull.Value())
 	}
 
 	return sess
diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go
index cc363d2fae..1bbc0eee56 100644
--- a/models/issues/issue_test.go
+++ b/models/issues/issue_test.go
@@ -379,7 +379,7 @@ func TestCountIssues(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	count, err := issues_model.CountIssues(db.DefaultContext, &issues_model.IssuesOptions{})
 	assert.NoError(t, err)
-	assert.EqualValues(t, 20, count)
+	assert.EqualValues(t, 22, count)
 }
 
 func TestIssueLoadAttributes(t *testing.T) {
diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go
index b258dc882c..ef96e1ee50 100644
--- a/models/issues/issue_update.go
+++ b/models/issues/issue_update.go
@@ -517,6 +517,15 @@ func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_mo
 	if err != nil {
 		return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
 	}
+
+	notBlocked := make([]*user_model.User, 0, len(mentions))
+	for _, user := range mentions {
+		if !user_model.IsUserBlockedBy(ctx, doer, user.ID) {
+			notBlocked = append(notBlocked, user)
+		}
+	}
+	mentions = notBlocked
+
 	if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
 		return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
 	}
diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go
index 77ef53a013..e2e35859df 100644
--- a/models/issues/issue_xref.go
+++ b/models/issues/issue_xref.go
@@ -46,10 +46,10 @@ func neuterCrossReferences(ctx context.Context, issueID, commentID int64) error
 	for i, c := range active {
 		ids[i] = c.ID
 	}
-	return neuterCrossReferencesIds(ctx, ids)
+	return neuterCrossReferencesIDs(ctx, ids)
 }
 
-func neuterCrossReferencesIds(ctx context.Context, ids []int64) error {
+func neuterCrossReferencesIDs(ctx context.Context, ids []int64) error {
 	_, err := db.GetEngine(ctx).In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: references.XRefActionNeutered})
 	return err
 }
@@ -100,7 +100,7 @@ func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossRefe
 			}
 		}
 		if len(ids) > 0 {
-			if err = neuterCrossReferencesIds(stdCtx, ids); err != nil {
+			if err = neuterCrossReferencesIDs(stdCtx, ids); err != nil {
 				return err
 			}
 		}
@@ -214,6 +214,10 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
 		if !perm.CanReadIssuesOrPulls(refIssue.IsPull) {
 			return nil, references.XRefActionNone, nil
 		}
+		if user_model.IsUserBlockedBy(stdCtx, ctx.Doer, refIssue.PosterID, refIssue.Repo.OwnerID) {
+			return nil, references.XRefActionNone, nil
+		}
+
 		// Accept close/reopening actions only if the poster is able to close the
 		// referenced issue manually at this moment. The only exception is
 		// the poster of a new PR referencing an issue on the same repo: then the merger
diff --git a/models/issues/label.go b/models/issues/label.go
index 527d8d7853..2397a29e35 100644
--- a/models/issues/label.go
+++ b/models/issues/label.go
@@ -12,6 +12,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/label"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
@@ -115,18 +116,23 @@ func (l *Label) CalOpenIssues() {
 func (l *Label) SetArchived(isArchived bool) {
 	if !isArchived {
 		l.ArchivedUnix = timeutil.TimeStamp(0)
-	} else if isArchived && l.ArchivedUnix.IsZero() {
+	} else if isArchived && !l.IsArchived() {
 		// Only change the date when it is newly archived.
 		l.ArchivedUnix = timeutil.TimeStampNow()
 	}
 }
 
+// IsArchived returns true if label is an archived
+func (l *Label) IsArchived() bool {
+	return !l.ArchivedUnix.IsZero()
+}
+
 // CalOpenOrgIssues calculates the open issues of a label for a specific repo
 func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
 	counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{
 		RepoIDs:  []int64{repoID},
 		LabelIDs: []int64{labelID},
-		IsClosed: util.OptionalBoolFalse,
+		IsClosed: optional.Some(false),
 	})
 
 	for _, count := range counts {
@@ -165,11 +171,6 @@ func (l *Label) BelongsToOrg() bool {
 	return l.OrgID > 0
 }
 
-// IsArchived returns true if label is an archived
-func (l *Label) IsArchived() bool {
-	return l.ArchivedUnix > 0
-}
-
 // BelongsToRepo returns true if label is a repository label
 func (l *Label) BelongsToRepo() bool {
 	return l.RepoID > 0
diff --git a/models/issues/milestone.go b/models/issues/milestone.go
index f663d42fe9..db0312adf0 100644
--- a/models/issues/milestone.go
+++ b/models/issues/milestone.go
@@ -6,10 +6,12 @@ package issues
 import (
 	"context"
 	"fmt"
+	"html/template"
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -47,8 +49,8 @@ type Milestone struct {
 	RepoID          int64                  `xorm:"INDEX"`
 	Repo            *repo_model.Repository `xorm:"-"`
 	Name            string
-	Content         string `xorm:"TEXT"`
-	RenderedContent string `xorm:"-"`
+	Content         string        `xorm:"TEXT"`
+	RenderedContent template.HTML `xorm:"-"`
 	IsClosed        bool
 	NumIssues       int
 	NumClosedIssues int
@@ -301,7 +303,7 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error {
 	}
 	numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
 		RepoID:   repo.ID,
-		IsClosed: util.OptionalBoolTrue,
+		IsClosed: optional.Some(true),
 	})
 	if err != nil {
 		return err
diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go
index a73bf73c17..d1b3f0301b 100644
--- a/models/issues/milestone_list.go
+++ b/models/issues/milestone_list.go
@@ -8,7 +8,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"xorm.io/builder"
 )
@@ -28,7 +28,7 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
 type FindMilestoneOptions struct {
 	db.ListOptions
 	RepoID   int64
-	IsClosed util.OptionalBool
+	IsClosed optional.Option[bool]
 	Name     string
 	SortType string
 	RepoCond builder.Cond
@@ -40,8 +40,8 @@ func (opts FindMilestoneOptions) ToConds() builder.Cond {
 	if opts.RepoID != 0 {
 		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
 	}
-	if opts.IsClosed != util.OptionalBoolNone {
-		cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.IsTrue()})
+	if opts.IsClosed.Has() {
+		cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()})
 	}
 	if opts.RepoCond != nil && opts.RepoCond.IsValid() {
 		cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond)))
diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go
index 7477af92c8..e5f6f15ca2 100644
--- a/models/issues/milestone_test.go
+++ b/models/issues/milestone_test.go
@@ -11,10 +11,10 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -39,10 +39,10 @@ func TestGetMilestoneByRepoID(t *testing.T) {
 func TestGetMilestonesByRepoID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	test := func(repoID int64, state api.StateType) {
-		var isClosed util.OptionalBool
+		var isClosed optional.Option[bool]
 		switch state {
 		case api.StateClosed, api.StateOpen:
-			isClosed = util.OptionalBoolOf(state == api.StateClosed)
+			isClosed = optional.Some(state == api.StateClosed)
 		}
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
 		milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
@@ -84,7 +84,7 @@ func TestGetMilestonesByRepoID(t *testing.T) {
 
 	milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 		RepoID:   unittest.NonexistentID,
-		IsClosed: util.OptionalBoolFalse,
+		IsClosed: optional.Some(false),
 	})
 	assert.NoError(t, err)
 	assert.Len(t, milestones, 0)
@@ -101,7 +101,7 @@ func TestGetMilestones(t *testing.T) {
 					PageSize: setting.UI.IssuePagingNum,
 				},
 				RepoID:   repo.ID,
-				IsClosed: util.OptionalBoolFalse,
+				IsClosed: optional.Some(false),
 				SortType: sortType,
 			})
 			assert.NoError(t, err)
@@ -118,7 +118,7 @@ func TestGetMilestones(t *testing.T) {
 					PageSize: setting.UI.IssuePagingNum,
 				},
 				RepoID:   repo.ID,
-				IsClosed: util.OptionalBoolTrue,
+				IsClosed: optional.Some(true),
 				Name:     "",
 				SortType: sortType,
 			})
@@ -178,7 +178,7 @@ func TestCountRepoClosedMilestones(t *testing.T) {
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
 		count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 			RepoID:   repoID,
-			IsClosed: util.OptionalBoolTrue,
+			IsClosed: optional.Some(true),
 		})
 		assert.NoError(t, err)
 		assert.EqualValues(t, repo.NumClosedMilestones, count)
@@ -189,7 +189,7 @@ func TestCountRepoClosedMilestones(t *testing.T) {
 
 	count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 		RepoID:   unittest.NonexistentID,
-		IsClosed: util.OptionalBoolTrue,
+		IsClosed: optional.Some(true),
 	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 0, count)
@@ -206,7 +206,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
 
 	openCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{
 		RepoIDs:  []int64{1, 2},
-		IsClosed: util.OptionalBoolFalse,
+		IsClosed: optional.Some(false),
 	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, repo1OpenCount, openCounts[1])
@@ -215,7 +215,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
 	closedCounts, err := issues_model.CountMilestonesMap(db.DefaultContext,
 		issues_model.FindMilestoneOptions{
 			RepoIDs:  []int64{1, 2},
-			IsClosed: util.OptionalBoolTrue,
+			IsClosed: optional.Some(true),
 		})
 	assert.NoError(t, err)
 	assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
@@ -234,7 +234,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
 					PageSize: setting.UI.IssuePagingNum,
 				},
 				RepoIDs:  []int64{repo1.ID, repo2.ID},
-				IsClosed: util.OptionalBoolFalse,
+				IsClosed: optional.Some(false),
 				SortType: sortType,
 			})
 			assert.NoError(t, err)
@@ -252,7 +252,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
 						PageSize: setting.UI.IssuePagingNum,
 					},
 					RepoIDs:  []int64{repo1.ID, repo2.ID},
-					IsClosed: util.OptionalBoolTrue,
+					IsClosed: optional.Some(true),
 					SortType: sortType,
 				})
 			assert.NoError(t, err)
diff --git a/models/issues/pull.go b/models/issues/pull.go
index 2cb1e1b971..dc1b1b956a 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -19,7 +19,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -652,6 +651,35 @@ func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest,
 	return pr, pr.LoadAttributes(ctx)
 }
 
+// GetPullRequestsByBaseHeadInfo returns the pull request by given base and head
+func GetPullRequestByBaseHeadInfo(ctx context.Context, baseID, headID int64, base, head string) (*PullRequest, error) {
+	pr := &PullRequest{}
+	sess := db.GetEngine(ctx).
+		Join("INNER", "issue", "issue.id = pull_request.issue_id").
+		Where("base_repo_id = ? AND base_branch = ? AND head_repo_id = ? AND head_branch = ?", baseID, base, headID, head)
+	has, err := sess.Get(pr)
+	if err != nil {
+		return nil, err
+	}
+	if !has {
+		return nil, ErrPullRequestNotExist{
+			HeadRepoID: headID,
+			BaseRepoID: baseID,
+			HeadBranch: head,
+			BaseBranch: base,
+		}
+	}
+
+	if err = pr.LoadAttributes(ctx); err != nil {
+		return nil, err
+	}
+	if err = pr.LoadIssue(ctx); err != nil {
+		return nil, err
+	}
+
+	return pr, nil
+}
+
 // GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request
 // By poster id.
 func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*PullRequest, error) {
@@ -855,82 +883,6 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *
 	return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
 }
 
-func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullRequest) error {
-	files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
-
-	if pr.IsWorkInProgress(ctx) {
-		return nil
-	}
-
-	if err := pr.LoadBaseRepo(ctx); err != nil {
-		return err
-	}
-
-	repo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
-	if err != nil {
-		return err
-	}
-	defer repo.Close()
-
-	branch, err := repo.GetDefaultBranch()
-	if err != nil {
-		return err
-	}
-
-	commit, err := repo.GetBranchCommit(branch)
-	if err != nil {
-		return err
-	}
-
-	var data string
-	for _, file := range files {
-		if blob, err := commit.GetBlobByPath(file); err == nil {
-			data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
-			if err == nil {
-				break
-			}
-		}
-	}
-
-	rules, _ := GetCodeOwnersFromContent(ctx, data)
-	changedFiles, err := repo.GetFilesChangedBetween(git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
-	if err != nil {
-		return err
-	}
-
-	uniqUsers := make(map[int64]*user_model.User)
-	uniqTeams := make(map[string]*org_model.Team)
-	for _, rule := range rules {
-		for _, f := range changedFiles {
-			if (rule.Rule.MatchString(f) && !rule.Negative) || (!rule.Rule.MatchString(f) && rule.Negative) {
-				for _, u := range rule.Users {
-					uniqUsers[u.ID] = u
-				}
-				for _, t := range rule.Teams {
-					uniqTeams[fmt.Sprintf("%d/%d", t.OrgID, t.ID)] = t
-				}
-			}
-		}
-	}
-
-	for _, u := range uniqUsers {
-		if u.ID != pull.Poster.ID {
-			if _, err := AddReviewRequest(ctx, pull, u, pull.Poster); err != nil {
-				log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
-				return err
-			}
-		}
-	}
-	for _, t := range uniqTeams {
-		if _, err := AddTeamReviewRequest(ctx, pull, t, pull.Poster); err != nil {
-			log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
-			return err
-		}
-	}
-
-	return nil
-}
-
 // GetCodeOwnersFromContent returns the code owners configuration
 // Return empty slice if files missing
 // Return warning messages on parsing errors
@@ -1093,3 +1045,23 @@ func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error {
 	}
 	return committer.Commit()
 }
+
+// GetPullRequestByMergedCommit returns a merged pull request by the given commit
+func GetPullRequestByMergedCommit(ctx context.Context, repoID int64, sha string) (*PullRequest, error) {
+	pr := new(PullRequest)
+	has, err := db.GetEngine(ctx).Where("base_repo_id = ? AND merged_commit_id = ?", repoID, sha).Get(pr)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""}
+	}
+
+	if err = pr.LoadAttributes(ctx); err != nil {
+		return nil, err
+	}
+	if err = pr.LoadIssue(ctx); err != nil {
+		return nil, err
+	}
+
+	return pr, nil
+}
diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go
index c209386e2e..de3eceed37 100644
--- a/models/issues/pull_list.go
+++ b/models/issues/pull_list.go
@@ -11,7 +11,6 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/util"
 
@@ -23,7 +22,7 @@ type PullRequestsOptions struct {
 	db.ListOptions
 	State       string
 	SortType    string
-	Labels      []string
+	Labels      []int64
 	MilestoneID int64
 }
 
@@ -36,11 +35,9 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
 		sess.And("issue.is_closed=?", opts.State == "closed")
 	}
 
-	if labelIDs, err := base.StringsToInt64s(opts.Labels); err != nil {
-		return nil, err
-	} else if len(labelIDs) > 0 {
+	if len(opts.Labels) > 0 {
 		sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id").
-			In("issue_label.label_id", labelIDs)
+			In("issue_label.label_id", opts.Labels)
 	}
 
 	if opts.MilestoneID > 0 {
@@ -212,3 +209,12 @@ func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bo
 		Limit(1).
 		Get(new(Issue))
 }
+
+// GetPullRequestByIssueIDs returns all pull requests by issue ids
+func GetPullRequestByIssueIDs(ctx context.Context, issueIDs []int64) (PullRequestList, error) {
+	prs := make([]*PullRequest, 0, len(issueIDs))
+	return prs, db.GetEngine(ctx).
+		Where("issue_id > 0").
+		In("issue_id", issueIDs).
+		Find(&prs)
+}
diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go
index 173417136c..675c90527d 100644
--- a/models/issues/pull_test.go
+++ b/models/issues/pull_test.go
@@ -66,7 +66,6 @@ func TestPullRequestsNewest(t *testing.T) {
 		},
 		State:    "open",
 		SortType: "newest",
-		Labels:   []string{},
 	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 3, count)
@@ -113,7 +112,6 @@ func TestPullRequestsOldest(t *testing.T) {
 		},
 		State:    "open",
 		SortType: "oldest",
-		Labels:   []string{},
 	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 3, count)
@@ -339,6 +337,18 @@ func TestGetApprovers(t *testing.T) {
 	assert.EqualValues(t, expected, approvers)
 }
 
+func TestGetPullRequestByMergedCommit(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	pr, err := issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3")
+	assert.NoError(t, err)
+	assert.EqualValues(t, 1, pr.ID)
+
+	_, err = issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 0, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3")
+	assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{})
+	_, err = issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 1, "")
+	assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{})
+}
+
 func TestMigrate_InsertPullRequests(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	reponame := "repo1"
diff --git a/models/issues/reaction.go b/models/issues/reaction.go
index bb47cf24ca..eb7faefc79 100644
--- a/models/issues/reaction.go
+++ b/models/issues/reaction.go
@@ -240,25 +240,6 @@ func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, erro
 	return reaction, nil
 }
 
-// CreateIssueReaction creates a reaction on issue.
-func CreateIssueReaction(ctx context.Context, doerID, issueID int64, content string) (*Reaction, error) {
-	return CreateReaction(ctx, &ReactionOptions{
-		Type:    content,
-		DoerID:  doerID,
-		IssueID: issueID,
-	})
-}
-
-// CreateCommentReaction creates a reaction on comment.
-func CreateCommentReaction(ctx context.Context, doerID, issueID, commentID int64, content string) (*Reaction, error) {
-	return CreateReaction(ctx, &ReactionOptions{
-		Type:      content,
-		DoerID:    doerID,
-		IssueID:   issueID,
-		CommentID: commentID,
-	})
-}
-
 // DeleteReaction deletes reaction for issue or comment.
 func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
 	reaction := &Reaction{
@@ -324,14 +305,12 @@ func (list ReactionList) GroupByType() map[string]ReactionList {
 }
 
 func (list ReactionList) getUserIDs() []int64 {
-	userIDs := make(container.Set[int64], len(list))
-	for _, reaction := range list {
+	return container.FilterSlice(list, func(reaction *Reaction) (int64, bool) {
 		if reaction.OriginalAuthor != "" {
-			continue
+			return 0, false
 		}
-		userIDs.Add(reaction.UserID)
-	}
-	return userIDs.Values()
+		return reaction.UserID, true
+	})
 }
 
 func valuesUser(m map[int64]*user_model.User) []*user_model.User {
diff --git a/models/issues/review.go b/models/issues/review.go
index f2022ae0aa..92764db4d1 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -66,6 +66,23 @@ func (err ErrNotValidReviewRequest) Unwrap() error {
 	return util.ErrInvalidArgument
 }
 
+// ErrReviewRequestOnClosedPR represents an error when an user tries to request a re-review on a closed or merged PR.
+type ErrReviewRequestOnClosedPR struct{}
+
+// IsErrReviewRequestOnClosedPR checks if an error is an ErrReviewRequestOnClosedPR.
+func IsErrReviewRequestOnClosedPR(err error) bool {
+	_, ok := err.(ErrReviewRequestOnClosedPR)
+	return ok
+}
+
+func (err ErrReviewRequestOnClosedPR) Error() string {
+	return "cannot request a re-review on a closed or merged PR"
+}
+
+func (err ErrReviewRequestOnClosedPR) Unwrap() error {
+	return util.ErrPermissionDenied
+}
+
 // ReviewType defines the sort of feedback a review gives
 type ReviewType int
 
@@ -159,6 +176,14 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) {
 		return err
 	}
 	r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID)
+	if err != nil {
+		if !user_model.IsErrUserNotExist(err) {
+			return fmt.Errorf("GetPossibleUserByID [%d]: %w", r.ReviewerID, err)
+		}
+		r.ReviewerID = user_model.GhostUserID
+		r.Reviewer = user_model.NewGhostUser()
+		return nil
+	}
 	return err
 }
 
@@ -231,11 +256,11 @@ type CreateReviewOptions struct {
 
 // IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals)
 func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.User) (bool, error) {
-	pr, err := GetPullRequestByIssueID(ctx, issue.ID)
-	if err != nil {
+	if err := issue.LoadPullRequest(ctx); err != nil {
 		return false, err
 	}
 
+	pr := issue.PullRequest
 	rule, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
 	if err != nil {
 		return false, err
@@ -263,11 +288,10 @@ func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.
 
 // IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals)
 func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organization.Team) (bool, error) {
-	pr, err := GetPullRequestByIssueID(ctx, issue.ID)
-	if err != nil {
+	if err := issue.LoadPullRequest(ctx); err != nil {
 		return false, err
 	}
-	pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
+	pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, issue.PullRequest.BaseRepoID, issue.PullRequest.BaseBranch)
 	if err != nil {
 		return false, err
 	}
@@ -284,8 +308,14 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio
 
 // CreateReview creates a new review based on opts
 func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) {
+	ctx, committer, err := db.TxContext(ctx)
+	if err != nil {
+		return nil, err
+	}
+	defer committer.Close()
+	sess := db.GetEngine(ctx)
+
 	review := &Review{
-		Type:         opts.Type,
 		Issue:        opts.Issue,
 		IssueID:      opts.Issue.ID,
 		Reviewer:     opts.Reviewer,
@@ -295,15 +325,39 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error
 		CommitID:     opts.CommitID,
 		Stale:        opts.Stale,
 	}
+
 	if opts.Reviewer != nil {
+		review.Type = opts.Type
 		review.ReviewerID = opts.Reviewer.ID
-	} else {
-		if review.Type != ReviewTypeRequest {
-			review.Type = ReviewTypeRequest
+
+		reviewCond := builder.Eq{"reviewer_id": opts.Reviewer.ID, "issue_id": opts.Issue.ID}
+		// make sure user review requests are cleared
+		if opts.Type != ReviewTypePending {
+			if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil {
+				return nil, err
+			}
 		}
+		// make sure if the created review gets dismissed no old review surface
+		// other types can be ignored, as they don't affect branch protection
+		if opts.Type == ReviewTypeApprove || opts.Type == ReviewTypeReject {
+			if _, err := sess.Where(reviewCond.And(builder.In("type", ReviewTypeApprove, ReviewTypeReject))).
+				Cols("dismissed").Update(&Review{Dismissed: true}); err != nil {
+				return nil, err
+			}
+		}
+
+	} else if opts.ReviewerTeam != nil {
+		review.Type = ReviewTypeRequest
 		review.ReviewerTeamID = opts.ReviewerTeam.ID
+
+	} else {
+		return nil, fmt.Errorf("provide either reviewer or reviewer team")
 	}
-	return review, db.Insert(ctx, review)
+
+	if _, err := sess.Insert(review); err != nil {
+		return nil, err
+	}
+	return review, committer.Commit()
 }
 
 // GetCurrentReview returns the current pending review of reviewer for given issue
@@ -581,9 +635,24 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
 		return nil, err
 	}
 
-	// skip it when reviewer hase been request to review
-	if review != nil && review.Type == ReviewTypeRequest {
-		return nil, nil
+	if review != nil {
+		// skip it when reviewer hase been request to review
+		if review.Type == ReviewTypeRequest {
+			return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
+		}
+
+		if issue.IsClosed {
+			return nil, ErrReviewRequestOnClosedPR{}
+		}
+
+		if issue.IsPull {
+			if err := issue.LoadPullRequest(ctx); err != nil {
+				return nil, err
+			}
+			if issue.PullRequest.HasMerged {
+				return nil, ErrReviewRequestOnClosedPR{}
+			}
+		}
 	}
 
 	// if the reviewer is an official reviewer,
@@ -621,6 +690,9 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
 		return nil, err
 	}
 
+	// func caller use the created comment to retrieve created review too.
+	comment.Review = review
+
 	return comment, committer.Commit()
 }
 
diff --git a/models/issues/review_list.go b/models/issues/review_list.go
index ed3d0bd028..7b8c3d319c 100644
--- a/models/issues/review_list.go
+++ b/models/issues/review_list.go
@@ -9,7 +9,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"xorm.io/builder"
 )
@@ -18,11 +18,11 @@ type ReviewList []*Review
 
 // LoadReviewers loads reviewers
 func (reviews ReviewList) LoadReviewers(ctx context.Context) error {
-	reviewerIds := make([]int64, len(reviews))
+	reviewerIDs := make([]int64, len(reviews))
 	for i := 0; i < len(reviews); i++ {
-		reviewerIds[i] = reviews[i].ReviewerID
+		reviewerIDs[i] = reviews[i].ReviewerID
 	}
-	reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIds)
+	reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIDs)
 	if err != nil {
 		return err
 	}
@@ -38,12 +38,11 @@ func (reviews ReviewList) LoadReviewers(ctx context.Context) error {
 }
 
 func (reviews ReviewList) LoadIssues(ctx context.Context) error {
-	issueIds := container.Set[int64]{}
-	for i := 0; i < len(reviews); i++ {
-		issueIds.Add(reviews[i].IssueID)
-	}
+	issueIDs := container.FilterSlice(reviews, func(review *Review) (int64, bool) {
+		return review.IssueID, true
+	})
 
-	issues, err := GetIssuesByIDs(ctx, issueIds.Values())
+	issues, err := GetIssuesByIDs(ctx, issueIDs)
 	if err != nil {
 		return err
 	}
@@ -68,7 +67,7 @@ type FindReviewOptions struct {
 	IssueID      int64
 	ReviewerID   int64
 	OfficialOnly bool
-	Dismissed    util.OptionalBool
+	Dismissed    optional.Option[bool]
 }
 
 func (opts *FindReviewOptions) toCond() builder.Cond {
@@ -85,8 +84,8 @@ func (opts *FindReviewOptions) toCond() builder.Cond {
 	if opts.OfficialOnly {
 		cond = cond.And(builder.Eq{"official": true})
 	}
-	if !opts.Dismissed.IsNone() {
-		cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.IsTrue()})
+	if opts.Dismissed.Has() {
+		cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.Value()})
 	}
 	return cond
 }
diff --git a/models/issues/review_test.go b/models/issues/review_test.go
index 1868cb1bfa..ac1b84adeb 100644
--- a/models/issues/review_test.go
+++ b/models/issues/review_test.go
@@ -288,3 +288,33 @@ func TestDeleteDismissedReview(t *testing.T) {
 	assert.NoError(t, issues_model.DeleteReview(db.DefaultContext, review))
 	unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID})
 }
+
+func TestAddReviewRequest(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
+	assert.NoError(t, pull.LoadIssue(db.DefaultContext))
+	issue := pull.Issue
+	assert.NoError(t, issue.LoadRepo(db.DefaultContext))
+	reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	_, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
+		Issue:    issue,
+		Reviewer: reviewer,
+		Type:     issues_model.ReviewTypeReject,
+	})
+
+	assert.NoError(t, err)
+	pull.HasMerged = false
+	assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
+	issue.IsClosed = true
+	_, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{})
+	assert.Error(t, err)
+	assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err))
+
+	pull.HasMerged = true
+	assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
+	issue.IsClosed = false
+	_, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{})
+	assert.Error(t, err)
+	assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err))
+}
diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go
index 91c4832e49..4063ca043b 100644
--- a/models/issues/tracked_time.go
+++ b/models/issues/tracked_time.go
@@ -11,6 +11,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 
@@ -340,7 +341,7 @@ func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) {
 }
 
 // GetIssueTotalTrackedTime returns the total tracked time for issues by given conditions.
-func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool) (int64, error) {
+func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool]) (int64, error) {
 	if len(opts.IssueIDs) <= MaxQueryParameters {
 		return getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs)
 	}
@@ -363,7 +364,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed
 	return accum, nil
 }
 
-func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool, issueIDs []int64) (int64, error) {
+func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool], issueIDs []int64) (int64, error) {
 	sumSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
 		sess := db.GetEngine(ctx).
 			Table("tracked_time").
@@ -378,8 +379,8 @@ func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isC
 	}
 
 	session := sumSession(opts, issueIDs)
-	if !isClosed.IsNone() {
-		session = session.And("issue.is_closed = ?", isClosed.IsTrue())
+	if isClosed.Has() {
+		session = session.And("issue.is_closed = ?", isClosed.Value())
 	}
 	return session.SumInt(new(trackedTime), "tracked_time.time")
 }
diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go
index 9beb862ffb..d82bff967a 100644
--- a/models/issues/tracked_time_test.go
+++ b/models/issues/tracked_time_test.go
@@ -11,7 +11,7 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -120,15 +120,15 @@ func TestTotalTimesForEachUser(t *testing.T) {
 func TestGetIssueTotalTrackedTime(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolFalse)
+	ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(false))
 	assert.NoError(t, err)
 	assert.EqualValues(t, 3682, ttt)
 
-	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolTrue)
+	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(true))
 	assert.NoError(t, err)
 	assert.EqualValues(t, 0, ttt)
 
-	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolNone)
+	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.None[bool]())
 	assert.NoError(t, err)
 	assert.EqualValues(t, 3682, ttt)
 }
diff --git a/models/migrations/base/db_test.go b/models/migrations/base/db_test.go
index 4d61b758cf..80bf00b22a 100644
--- a/models/migrations/base/db_test.go
+++ b/models/migrations/base/db_test.go
@@ -36,12 +36,14 @@ func Test_DropTableColumns(t *testing.T) {
 		"updated_unix",
 	}
 
+	x.SetMapper(names.GonicMapper{})
+
 	for i := range columns {
-		x.SetMapper(names.GonicMapper{})
 		if err := x.Sync(new(DropTest)); err != nil {
 			t.Errorf("unable to create DropTest table: %v", err)
 			return
 		}
+
 		sess := x.NewSession()
 		if err := sess.Begin(); err != nil {
 			sess.Close()
@@ -64,7 +66,6 @@ func Test_DropTableColumns(t *testing.T) {
 			return
 		}
 		for j := range columns[i+1:] {
-			x.SetMapper(names.GonicMapper{})
 			if err := x.Sync(new(DropTest)); err != nil {
 				t.Errorf("unable to create DropTest table: %v", err)
 				return
diff --git a/models/migrations/base/hash.go b/models/migrations/base/hash.go
index 0debec272b..00fd1efd4a 100644
--- a/models/migrations/base/hash.go
+++ b/models/migrations/base/hash.go
@@ -4,9 +4,9 @@
 package base
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 
-	"github.com/minio/sha256-simd"
 	"golang.org/x/crypto/pbkdf2"
 )
 
diff --git a/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml b/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml
new file mode 100644
index 0000000000..f95d47916b
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml
@@ -0,0 +1,4 @@
+-
+  id: 1
+  repo_id: 1
+  index: 1
diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml
new file mode 100644
index 0000000000..056236ba9e
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml
@@ -0,0 +1,11 @@
+-
+  id: 1
+  uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
+  issue_id: 1
+  release_id: 0
+
+-
+  id: 2
+  uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12
+  issue_id: 0
+  release_id: 1
diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml
new file mode 100644
index 0000000000..7f3255096d
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  repo_id: 1
diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml
new file mode 100644
index 0000000000..7f3255096d
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  repo_id: 1
diff --git a/models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml b/models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml
new file mode 100644
index 0000000000..6feaeb39f0
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml
@@ -0,0 +1,9 @@
+-
+  id: 1
+  project_id: 1
+  issue_id: 1
+
+-
+  id: 2
+  project_id: 1
+  issue_id: 1
diff --git a/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project.yml b/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project.yml
new file mode 100644
index 0000000000..2450d20beb
--- /dev/null
+++ b/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project.yml
@@ -0,0 +1,23 @@
+-
+  id: 1
+  title: project without default column
+  owner_id: 2
+  repo_id: 0
+  is_closed: false
+  creator_id: 2
+  board_type: 1
+  type: 2
+  created_unix: 1688973000
+  updated_unix: 1688973000
+
+-
+  id: 2
+  title: project with multiple default columns
+  owner_id: 2
+  repo_id: 0
+  is_closed: false
+  creator_id: 2
+  board_type: 1
+  type: 2
+  created_unix: 1688973000
+  updated_unix: 1688973000
diff --git a/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml b/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml
new file mode 100644
index 0000000000..2e1b1c7eee
--- /dev/null
+++ b/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml
@@ -0,0 +1,26 @@
+-
+  id: 1
+  project_id: 1
+  title: Done
+  creator_id: 2
+  default: false
+  created_unix: 1588117528
+  updated_unix: 1588117528
+
+-
+  id: 2
+  project_id: 2
+  title: Backlog
+  creator_id: 2
+  default: true
+  created_unix: 1588117528
+  updated_unix: 1588117528
+
+-
+  id: 3
+  project_id: 2
+  title: Uncategorized
+  creator_id: 2
+  default: true
+  created_unix: 1588117528
+  updated_unix: 1588117528
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/comment.yml b/models/migrations/fixtures/Test_RepositoryFormat/comment.yml
new file mode 100644
index 0000000000..1197b086e3
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/comment.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml b/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml
new file mode 100644
index 0000000000..ca0aaec4cc
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  context_hash: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml b/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml
new file mode 100644
index 0000000000..380cc079ee
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml
@@ -0,0 +1,5 @@
+-
+  id: 1
+  commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
+  merge_base: 19fe5caf872476db265596eaac1dc35ad1c6422d
+  merged_commit_id: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/release.yml b/models/migrations/fixtures/Test_RepositoryFormat/release.yml
new file mode 100644
index 0000000000..ffabe4ab9e
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/release.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  sha1: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml b/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml
new file mode 100644
index 0000000000..f04cb3b340
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  commit_id: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml b/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml
new file mode 100644
index 0000000000..1197b086e3
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml b/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml
new file mode 100644
index 0000000000..1197b086e3
--- /dev/null
+++ b/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml
@@ -0,0 +1,3 @@
+-
+  id: 1
+  commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
diff --git a/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml b/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml
new file mode 100644
index 0000000000..7025144106
--- /dev/null
+++ b/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml
@@ -0,0 +1,4 @@
+-
+  id: 1
+  description: the badge
+  image_url: https://gitea.com/myimage.png
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index beb1f3bb96..3ea8f2acbf 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -21,6 +21,7 @@ import (
 	"code.gitea.io/gitea/models/migrations/v1_20"
 	"code.gitea.io/gitea/models/migrations/v1_21"
 	"code.gitea.io/gitea/models/migrations/v1_22"
+	"code.gitea.io/gitea/models/migrations/v1_23"
 	"code.gitea.io/gitea/models/migrations/v1_6"
 	"code.gitea.io/gitea/models/migrations/v1_7"
 	"code.gitea.io/gitea/models/migrations/v1_8"
@@ -558,6 +559,27 @@ var migrations = []Migration{
 	NewMigration("Add PreviousDuration to ActionRun", v1_22.AddPreviousDurationToActionRun),
 	// v286 -> v287
 	NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256),
+	// v287 -> v288
+	NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
+	// v288 -> v289
+	NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable),
+	// v289 -> v290
+	NewMigration("Add default_wiki_branch to repository table", v1_22.AddDefaultWikiBranch),
+	// v290 -> v291
+	NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable),
+	// v291 -> v292
+	NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment),
+	// v292 -> v293
+	NewMigration("Ensure every project has exactly one default column - No Op", noopMigration),
+	// v293 -> v294
+	NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
+
+	// Gitea 1.22.0 ends at 294
+
+	// v294 -> v295
+	NewMigration("Add unique index for project issue table", v1_23.AddUniqueIndexForProjectIssue),
+	// v295 -> v296
+	NewMigration("Add commit status summary table", v1_23.AddCommitStatusSummary),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_14/v166.go b/models/migrations/v1_14/v166.go
index 78f33e8f9b..e5731582fd 100644
--- a/models/migrations/v1_14/v166.go
+++ b/models/migrations/v1_14/v166.go
@@ -4,9 +4,9 @@
 package v1_14 //nolint
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 
-	"github.com/minio/sha256-simd"
 	"golang.org/x/crypto/argon2"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/crypto/pbkdf2"
diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go
index 17669a012e..d99bbc2962 100644
--- a/models/migrations/v1_16/v193_test.go
+++ b/models/migrations/v1_16/v193_test.go
@@ -15,7 +15,6 @@ func Test_AddRepoIDForAttachment(t *testing.T) {
 	type Attachment struct {
 		ID         int64  `xorm:"pk autoincr"`
 		UUID       string `xorm:"uuid UNIQUE"`
-		RepoID     int64  `xorm:"INDEX"` // this should not be zero
 		IssueID    int64  `xorm:"INDEX"` // maybe zero when creating
 		ReleaseID  int64  `xorm:"INDEX"` // maybe zero when creating
 		UploaderID int64  `xorm:"INDEX DEFAULT 0"`
@@ -44,12 +43,21 @@ func Test_AddRepoIDForAttachment(t *testing.T) {
 		return
 	}
 
-	var issueAttachments []*Attachment
-	err := x.Where("issue_id > 0").Find(&issueAttachments)
+	type NewAttachment struct {
+		ID         int64  `xorm:"pk autoincr"`
+		UUID       string `xorm:"uuid UNIQUE"`
+		RepoID     int64  `xorm:"INDEX"` // this should not be zero
+		IssueID    int64  `xorm:"INDEX"` // maybe zero when creating
+		ReleaseID  int64  `xorm:"INDEX"` // maybe zero when creating
+		UploaderID int64  `xorm:"INDEX DEFAULT 0"`
+	}
+
+	var issueAttachments []*NewAttachment
+	err := x.Table("attachment").Where("issue_id > 0").Find(&issueAttachments)
 	assert.NoError(t, err)
 	for _, attach := range issueAttachments {
-		assert.Greater(t, attach.RepoID, 0)
-		assert.Greater(t, attach.IssueID, 0)
+		assert.Greater(t, attach.RepoID, int64(0))
+		assert.Greater(t, attach.IssueID, int64(0))
 		var issue Issue
 		has, err := x.ID(attach.IssueID).Get(&issue)
 		assert.NoError(t, err)
@@ -57,12 +65,12 @@ func Test_AddRepoIDForAttachment(t *testing.T) {
 		assert.EqualValues(t, attach.RepoID, issue.RepoID)
 	}
 
-	var releaseAttachments []*Attachment
-	err = x.Where("release_id > 0").Find(&releaseAttachments)
+	var releaseAttachments []*NewAttachment
+	err = x.Table("attachment").Where("release_id > 0").Find(&releaseAttachments)
 	assert.NoError(t, err)
 	for _, attach := range releaseAttachments {
-		assert.Greater(t, attach.RepoID, 0)
-		assert.Greater(t, attach.IssueID, 0)
+		assert.Greater(t, attach.RepoID, int64(0))
+		assert.Greater(t, attach.ReleaseID, int64(0))
 		var release Release
 		has, err := x.ID(attach.ReleaseID).Get(&release)
 		assert.NoError(t, err)
diff --git a/models/migrations/v1_22/v283.go b/models/migrations/v1_22/v283.go
index b2b94845d9..0a45c51245 100644
--- a/models/migrations/v1_22/v283.go
+++ b/models/migrations/v1_22/v283.go
@@ -4,7 +4,10 @@
 package v1_22 //nolint
 
 import (
+	"fmt"
+
 	"xorm.io/xorm"
+	"xorm.io/xorm/schemas"
 )
 
 func AddCombinedIndexToIssueUser(x *xorm.Engine) error {
@@ -20,8 +23,18 @@ func AddCombinedIndexToIssueUser(x *xorm.Engine) error {
 		return err
 	}
 	for _, issueUser := range duplicatedIssueUsers {
-		if _, err := x.Exec("delete from issue_user where id in (SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?)", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1); err != nil {
-			return err
+		if x.Dialect().URI().DBType == schemas.MSSQL {
+			if _, err := x.Exec(fmt.Sprintf("delete from issue_user where id in (SELECT top %d id FROM issue_user WHERE issue_id = ? and uid = ?)", issueUser.Cnt-1), issueUser.IssueID, issueUser.UID); err != nil {
+				return err
+			}
+		} else {
+			var ids []int64
+			if err := x.SQL("SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1).Find(&ids); err != nil {
+				return err
+			}
+			if _, err := x.Table("issue_user").In("id", ids).Delete(); err != nil {
+				return err
+			}
 		}
 	}
 
diff --git a/models/migrations/v1_22/v286.go b/models/migrations/v1_22/v286.go
index ef19f64221..fbbd87344f 100644
--- a/models/migrations/v1_22/v286.go
+++ b/models/migrations/v1_22/v286.go
@@ -36,9 +36,9 @@ func expandHashReferencesToSha256(x *xorm.Engine) error {
 		if setting.Database.Type.IsMSSQL() {
 			// drop indexes that need to be re-created afterwards
 			droppedIndexes := []string{
-				"DROP INDEX commit_status.IDX_commit_status_context_hash",
-				"DROP INDEX review_state.UQE_review_state_pull_commit_user",
-				"DROP INDEX repo_archiver.UQE_repo_archiver_s",
+				"DROP INDEX IF EXISTS [IDX_commit_status_context_hash] ON [commit_status]",
+				"DROP INDEX IF EXISTS [UQE_review_state_pull_commit_user] ON [review_state]",
+				"DROP INDEX IF EXISTS [UQE_repo_archiver_s] ON [repo_archiver]",
 			}
 			for _, s := range droppedIndexes {
 				_, err := db.Exec(s)
@@ -53,7 +53,7 @@ func expandHashReferencesToSha256(x *xorm.Engine) error {
 			if setting.Database.Type.IsMySQL() {
 				_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(64)", alts[0], alts[1]))
 			} else if setting.Database.Type.IsMSSQL() {
-				_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` VARCHAR(64)", alts[0], alts[1]))
+				_, err = db.Exec(fmt.Sprintf("ALTER TABLE [%s] ALTER COLUMN [%s] VARCHAR(64)", alts[0], alts[1]))
 			} else {
 				_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` TYPE VARCHAR(64)", alts[0], alts[1]))
 			}
diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go
index e36a18a116..7c353747e3 100644
--- a/models/migrations/v1_22/v286_test.go
+++ b/models/migrations/v1_22/v286_test.go
@@ -17,14 +17,72 @@ func PrepareOldRepository(t *testing.T) (*xorm.Engine, func()) {
 		ID int64 `xorm:"pk autoincr"`
 	}
 
+	type CommitStatus struct {
+		ID          int64
+		ContextHash string
+	}
+
+	type RepoArchiver struct {
+		ID       int64
+		RepoID   int64
+		Type     int
+		CommitID string
+	}
+
+	type ReviewState struct {
+		ID        int64
+		CommitSHA string
+		UserID    int64
+		PullID    int64
+	}
+
+	type Comment struct {
+		ID        int64
+		CommitSHA string
+	}
+
+	type PullRequest struct {
+		ID             int64
+		CommitSHA      string
+		MergeBase      string
+		MergedCommitID string
+	}
+
+	type Release struct {
+		ID   int64
+		Sha1 string
+	}
+
+	type RepoIndexerStatus struct {
+		ID        int64
+		CommitSHA string
+	}
+
+	type Review struct {
+		ID       int64
+		CommitID string
+	}
+
 	// Prepare and load the testing database
-	return base.PrepareTestEnv(t, 0, new(Repository))
+	return base.PrepareTestEnv(t, 0,
+		new(Repository),
+		new(CommitStatus),
+		new(RepoArchiver),
+		new(ReviewState),
+		new(Review),
+		new(Comment),
+		new(PullRequest),
+		new(Release),
+		new(RepoIndexerStatus),
+	)
 }
 
 func Test_RepositoryFormat(t *testing.T) {
 	x, deferable := PrepareOldRepository(t)
 	defer deferable()
 
+	assert.NoError(t, AdjustDBForSha256(x))
+
 	type Repository struct {
 		ID               int64  `xorm:"pk autoincr"`
 		ObjectFormatName string `xorg:"not null default('sha1')"`
@@ -37,12 +95,10 @@ func Test_RepositoryFormat(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, 4, count)
 
-	assert.NoError(t, AdjustDBForSha256(x))
-
-	repo.ID = 20
 	repo.ObjectFormatName = "sha256"
 	_, err = x.Insert(repo)
 	assert.NoError(t, err)
+	id := repo.ID
 
 	count, err = x.Count(new(Repository))
 	assert.NoError(t, err)
@@ -55,7 +111,7 @@ func Test_RepositoryFormat(t *testing.T) {
 	assert.EqualValues(t, "sha1", repo.ObjectFormatName)
 
 	repo = new(Repository)
-	ok, err = x.ID(20).Get(repo)
+	ok, err = x.ID(id).Get(repo)
 	assert.NoError(t, err)
 	assert.EqualValues(t, true, ok)
 	assert.EqualValues(t, "sha256", repo.ObjectFormatName)
diff --git a/models/migrations/v1_22/v287.go b/models/migrations/v1_22/v287.go
new file mode 100644
index 0000000000..c8b1593286
--- /dev/null
+++ b/models/migrations/v1_22/v287.go
@@ -0,0 +1,46 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"xorm.io/xorm"
+)
+
+type BadgeUnique struct {
+	ID   int64  `xorm:"pk autoincr"`
+	Slug string `xorm:"UNIQUE"`
+}
+
+func (BadgeUnique) TableName() string {
+	return "badge"
+}
+
+func UseSlugInsteadOfIDForBadges(x *xorm.Engine) error {
+	type Badge struct {
+		Slug string
+	}
+
+	err := x.Sync(new(Badge))
+	if err != nil {
+		return err
+	}
+
+	sess := x.NewSession()
+	defer sess.Close()
+	if err := sess.Begin(); err != nil {
+		return err
+	}
+
+	_, err = sess.Exec("UPDATE `badge` SET `slug` = `id` Where `slug` IS NULL")
+	if err != nil {
+		return err
+	}
+
+	err = sess.Sync(new(BadgeUnique))
+	if err != nil {
+		return err
+	}
+
+	return sess.Commit()
+}
diff --git a/models/migrations/v1_22/v287_test.go b/models/migrations/v1_22/v287_test.go
new file mode 100644
index 0000000000..9c7b10947d
--- /dev/null
+++ b/models/migrations/v1_22/v287_test.go
@@ -0,0 +1,57 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"fmt"
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_UpdateBadgeColName(t *testing.T) {
+	type Badge struct {
+		ID          int64 `xorm:"pk autoincr"`
+		Description string
+		ImageURL    string
+	}
+
+	// Prepare and load the testing database
+	x, deferable := base.PrepareTestEnv(t, 0, new(Badge))
+	defer deferable()
+	if x == nil || t.Failed() {
+		return
+	}
+
+	oldBadges := []*Badge{
+		{Description: "Test Badge 1", ImageURL: "https://example.com/badge1.png"},
+		{Description: "Test Badge 2", ImageURL: "https://example.com/badge2.png"},
+		{Description: "Test Badge 3", ImageURL: "https://example.com/badge3.png"},
+	}
+
+	for _, badge := range oldBadges {
+		_, err := x.Insert(badge)
+		assert.NoError(t, err)
+	}
+
+	if err := UseSlugInsteadOfIDForBadges(x); err != nil {
+		assert.NoError(t, err)
+		return
+	}
+
+	got := []BadgeUnique{}
+	if err := x.Table("badge").Asc("id").Find(&got); !assert.NoError(t, err) {
+		return
+	}
+
+	for i, e := range oldBadges {
+		got := got[i+1] // 1 is in the badge.yml
+		assert.Equal(t, e.ID, got.ID)
+		assert.Equal(t, fmt.Sprintf("%d", e.ID), got.Slug)
+	}
+
+	// TODO: check if badges have been updated
+}
diff --git a/models/migrations/v1_22/v288.go b/models/migrations/v1_22/v288.go
new file mode 100644
index 0000000000..7c93bfcc66
--- /dev/null
+++ b/models/migrations/v1_22/v288.go
@@ -0,0 +1,26 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"code.gitea.io/gitea/modules/timeutil"
+
+	"xorm.io/xorm"
+)
+
+type Blocking struct {
+	ID          int64 `xorm:"pk autoincr"`
+	BlockerID   int64 `xorm:"UNIQUE(block)"`
+	BlockeeID   int64 `xorm:"UNIQUE(block)"`
+	Note        string
+	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+}
+
+func (*Blocking) TableName() string {
+	return "user_blocking"
+}
+
+func AddUserBlockingTable(x *xorm.Engine) error {
+	return x.Sync(&Blocking{})
+}
diff --git a/models/migrations/v1_22/v289.go b/models/migrations/v1_22/v289.go
new file mode 100644
index 0000000000..e2dfc48715
--- /dev/null
+++ b/models/migrations/v1_22/v289.go
@@ -0,0 +1,18 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import "xorm.io/xorm"
+
+func AddDefaultWikiBranch(x *xorm.Engine) error {
+	type Repository struct {
+		ID                int64
+		DefaultWikiBranch string
+	}
+	if err := x.Sync(&Repository{}); err != nil {
+		return err
+	}
+	_, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')")
+	return err
+}
diff --git a/models/migrations/v1_22/v290.go b/models/migrations/v1_22/v290.go
new file mode 100644
index 0000000000..e9b7f504ba
--- /dev/null
+++ b/models/migrations/v1_22/v290.go
@@ -0,0 +1,17 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"xorm.io/xorm"
+)
+
+type HookTask struct {
+	PayloadVersion int `xorm:"DEFAULT 1"`
+}
+
+func AddPayloadVersionToHookTaskTable(x *xorm.Engine) error {
+	// create missing column
+	return x.Sync(new(HookTask))
+}
diff --git a/models/migrations/v1_22/v291.go b/models/migrations/v1_22/v291.go
new file mode 100644
index 0000000000..0bfffe5d05
--- /dev/null
+++ b/models/migrations/v1_22/v291.go
@@ -0,0 +1,14 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import "xorm.io/xorm"
+
+func AddCommentIDIndexofAttachment(x *xorm.Engine) error {
+	type Attachment struct {
+		CommentID int64 `xorm:"INDEX"`
+	}
+
+	return x.Sync(&Attachment{})
+}
diff --git a/models/migrations/v1_22/v292.go b/models/migrations/v1_22/v292.go
new file mode 100644
index 0000000000..beca556aee
--- /dev/null
+++ b/models/migrations/v1_22/v292.go
@@ -0,0 +1,9 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+// NOTE: noop the original migration has bug which some projects will be skip, so
+// these projects will have no default board.
+// So that this migration will be skipped and go to v293.go
+// This file is a placeholder so that readers can know what happened
diff --git a/models/migrations/v1_22/v293.go b/models/migrations/v1_22/v293.go
new file mode 100644
index 0000000000..53cc719294
--- /dev/null
+++ b/models/migrations/v1_22/v293.go
@@ -0,0 +1,108 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/timeutil"
+
+	"xorm.io/xorm"
+)
+
+// CheckProjectColumnsConsistency ensures there is exactly one default board per project present
+func CheckProjectColumnsConsistency(x *xorm.Engine) error {
+	sess := x.NewSession()
+	defer sess.Close()
+
+	limit := setting.Database.IterateBufferSize
+	if limit <= 0 {
+		limit = 50
+	}
+
+	type Project struct {
+		ID        int64
+		CreatorID int64
+		BoardID   int64
+	}
+
+	type ProjectBoard struct {
+		ID      int64 `xorm:"pk autoincr"`
+		Title   string
+		Default bool   `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board
+		Sorting int8   `xorm:"NOT NULL DEFAULT 0"`
+		Color   string `xorm:"VARCHAR(7)"`
+
+		ProjectID int64 `xorm:"INDEX NOT NULL"`
+		CreatorID int64 `xorm:"NOT NULL"`
+
+		CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+		UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
+	}
+
+	for {
+		if err := sess.Begin(); err != nil {
+			return err
+		}
+
+		// all these projects without defaults will be fixed in the same loop, so
+		// we just need to always get projects without defaults until no such project
+		var projects []*Project
+		if err := sess.Select("project.id as id, project.creator_id, project_board.id as board_id").
+			Join("LEFT", "project_board", "project_board.project_id = project.id AND project_board.`default`=?", true).
+			Where("project_board.id is NULL OR project_board.id = 0").
+			Limit(limit).
+			Find(&projects); err != nil {
+			return err
+		}
+
+		for _, p := range projects {
+			if _, err := sess.Insert(ProjectBoard{
+				ProjectID: p.ID,
+				Default:   true,
+				Title:     "Uncategorized",
+				CreatorID: p.CreatorID,
+			}); err != nil {
+				return err
+			}
+		}
+		if err := sess.Commit(); err != nil {
+			return err
+		}
+
+		if len(projects) == 0 {
+			break
+		}
+	}
+	sess.Close()
+
+	return removeDuplicatedBoardDefault(x)
+}
+
+func removeDuplicatedBoardDefault(x *xorm.Engine) error {
+	type ProjectInfo struct {
+		ProjectID  int64
+		DefaultNum int
+	}
+	var projects []ProjectInfo
+	if err := x.Select("project_id, count(*) AS default_num").
+		Table("project_board").
+		Where("`default` = ?", true).
+		GroupBy("project_id").
+		Having("count(*) > 1").
+		Find(&projects); err != nil {
+		return err
+	}
+
+	for _, project := range projects {
+		if _, err := x.Where("project_id=?", project.ProjectID).
+			Table("project_board").
+			Limit(project.DefaultNum - 1).
+			Update(map[string]bool{
+				"`default`": false,
+			}); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/models/migrations/v1_22/v293_test.go b/models/migrations/v1_22/v293_test.go
new file mode 100644
index 0000000000..ccc92f39a6
--- /dev/null
+++ b/models/migrations/v1_22/v293_test.go
@@ -0,0 +1,44 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/migrations/base"
+	"code.gitea.io/gitea/models/project"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_CheckProjectColumnsConsistency(t *testing.T) {
+	// Prepare and load the testing database
+	x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Board))
+	defer deferable()
+	if x == nil || t.Failed() {
+		return
+	}
+
+	assert.NoError(t, CheckProjectColumnsConsistency(x))
+
+	// check if default board was added
+	var defaultBoard project.Board
+	has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultBoard)
+	assert.NoError(t, err)
+	assert.True(t, has)
+	assert.Equal(t, int64(1), defaultBoard.ProjectID)
+	assert.True(t, defaultBoard.Default)
+
+	// check if multiple defaults, previous were removed and last will be kept
+	expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2)
+	assert.NoError(t, err)
+	assert.Equal(t, int64(2), expectDefaultBoard.ProjectID)
+	assert.False(t, expectDefaultBoard.Default)
+
+	expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3)
+	assert.NoError(t, err)
+	assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID)
+	assert.True(t, expectNonDefaultBoard.Default)
+}
diff --git a/models/migrations/v1_23/main_test.go b/models/migrations/v1_23/main_test.go
new file mode 100644
index 0000000000..b7948bd4dd
--- /dev/null
+++ b/models/migrations/v1_23/main_test.go
@@ -0,0 +1,14 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+)
+
+func TestMain(m *testing.M) {
+	base.MainTest(m)
+}
diff --git a/models/migrations/v1_23/v294.go b/models/migrations/v1_23/v294.go
new file mode 100644
index 0000000000..f2a54f6d23
--- /dev/null
+++ b/models/migrations/v1_23/v294.go
@@ -0,0 +1,53 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import (
+	"fmt"
+
+	"xorm.io/xorm"
+	"xorm.io/xorm/schemas"
+)
+
+// AddUniqueIndexForProjectIssue adds unique indexes for project issue table
+func AddUniqueIndexForProjectIssue(x *xorm.Engine) error {
+	// remove possible duplicated records in table project_issue
+	type result struct {
+		IssueID   int64
+		ProjectID int64
+		Cnt       int
+	}
+	var results []result
+	if err := x.Select("issue_id, project_id, count(*) as cnt").
+		Table("project_issue").
+		GroupBy("issue_id, project_id").
+		Having("count(*) > 1").
+		Find(&results); err != nil {
+		return err
+	}
+	for _, r := range results {
+		if x.Dialect().URI().DBType == schemas.MSSQL {
+			if _, err := x.Exec(fmt.Sprintf("delete from project_issue where id in (SELECT top %d id FROM project_issue WHERE issue_id = ? and project_id = ?)", r.Cnt-1), r.IssueID, r.ProjectID); err != nil {
+				return err
+			}
+		} else {
+			var ids []int64
+			if err := x.SQL("SELECT id FROM project_issue WHERE issue_id = ? and project_id = ? limit ?", r.IssueID, r.ProjectID, r.Cnt-1).Find(&ids); err != nil {
+				return err
+			}
+			if _, err := x.Table("project_issue").In("id", ids).Delete(); err != nil {
+				return err
+			}
+		}
+	}
+
+	// add unique index for project_issue table
+	type ProjectIssue struct { //revive:disable-line:exported
+		ID        int64 `xorm:"pk autoincr"`
+		IssueID   int64 `xorm:"INDEX unique(s)"`
+		ProjectID int64 `xorm:"INDEX unique(s)"`
+	}
+
+	return x.Sync(new(ProjectIssue))
+}
diff --git a/models/migrations/v1_23/v294_test.go b/models/migrations/v1_23/v294_test.go
new file mode 100644
index 0000000000..d9a44ad866
--- /dev/null
+++ b/models/migrations/v1_23/v294_test.go
@@ -0,0 +1,52 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import (
+	"slices"
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+
+	"github.com/stretchr/testify/assert"
+	"xorm.io/xorm/schemas"
+)
+
+func Test_AddUniqueIndexForProjectIssue(t *testing.T) {
+	type ProjectIssue struct { //revive:disable-line:exported
+		ID        int64 `xorm:"pk autoincr"`
+		IssueID   int64 `xorm:"INDEX"`
+		ProjectID int64 `xorm:"INDEX"`
+	}
+
+	// Prepare and load the testing database
+	x, deferable := base.PrepareTestEnv(t, 0, new(ProjectIssue))
+	defer deferable()
+	if x == nil || t.Failed() {
+		return
+	}
+
+	cnt, err := x.Table("project_issue").Where("project_id=1 AND issue_id=1").Count()
+	assert.NoError(t, err)
+	assert.EqualValues(t, 2, cnt)
+
+	assert.NoError(t, AddUniqueIndexForProjectIssue(x))
+
+	cnt, err = x.Table("project_issue").Where("project_id=1 AND issue_id=1").Count()
+	assert.NoError(t, err)
+	assert.EqualValues(t, 1, cnt)
+
+	tables, err := x.DBMetas()
+	assert.NoError(t, err)
+	assert.EqualValues(t, 1, len(tables))
+	found := false
+	for _, index := range tables[0].Indexes {
+		if index.Type == schemas.UniqueType {
+			found = true
+			slices.Equal(index.Cols, []string{"project_id", "issue_id"})
+			break
+		}
+	}
+	assert.True(t, found)
+}
diff --git a/models/migrations/v1_23/v295.go b/models/migrations/v1_23/v295.go
new file mode 100644
index 0000000000..9a2003cfc1
--- /dev/null
+++ b/models/migrations/v1_23/v295.go
@@ -0,0 +1,18 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import "xorm.io/xorm"
+
+func AddCommitStatusSummary(x *xorm.Engine) error {
+	type CommitStatusSummary struct {
+		ID     int64  `xorm:"pk autoincr"`
+		RepoID int64  `xorm:"INDEX UNIQUE(repo_id_sha)"`
+		SHA    string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"`
+		State  string `xorm:"VARCHAR(7) NOT NULL"`
+	}
+	// there is no migrations because if there is no data on this table, it will fall back to get data
+	// from commit status
+	return x.Sync2(new(CommitStatusSummary))
+}
diff --git a/models/org.go b/models/org.go
index 5f61f05b16..69cc47137e 100644
--- a/models/org.go
+++ b/models/org.go
@@ -12,15 +12,16 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
 )
 
 // RemoveOrgUser removes user from given organization.
-func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
+func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *user_model.User) error {
 	ou := new(organization.OrgUser)
 
 	has, err := db.GetEngine(ctx).
-		Where("uid=?", userID).
-		And("org_id=?", orgID).
+		Where("uid=?", user.ID).
+		And("org_id=?", org.ID).
 		Get(ou)
 	if err != nil {
 		return fmt.Errorf("get org-user: %w", err)
@@ -28,13 +29,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 		return nil
 	}
 
-	org, err := organization.GetOrgByID(ctx, orgID)
-	if err != nil {
-		return fmt.Errorf("GetUserByID [%d]: %w", orgID, err)
-	}
-
 	// Check if the user to delete is the last member in owner team.
-	if isOwner, err := organization.IsOrganizationOwner(ctx, orgID, userID); err != nil {
+	if isOwner, err := organization.IsOrganizationOwner(ctx, org.ID, user.ID); err != nil {
 		return err
 	} else if isOwner {
 		t, err := organization.GetOwnerTeam(ctx, org.ID)
@@ -45,8 +41,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 			if err := t.LoadMembers(ctx); err != nil {
 				return err
 			}
-			if t.Members[0].ID == userID {
-				return organization.ErrLastOrgOwner{UID: userID}
+			if t.Members[0].ID == user.ID {
+				return organization.ErrLastOrgOwner{UID: user.ID}
 			}
 		}
 	}
@@ -59,28 +55,32 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 
 	if _, err := db.DeleteByID[organization.OrgUser](ctx, ou.ID); err != nil {
 		return err
-	} else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil {
+	} else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", org.ID); err != nil {
 		return err
 	}
 
 	// Delete all repository accesses and unwatch them.
-	env, err := organization.AccessibleReposEnv(ctx, org, userID)
+	env, err := organization.AccessibleReposEnv(ctx, org, user.ID)
 	if err != nil {
 		return fmt.Errorf("AccessibleReposEnv: %w", err)
 	}
 	repoIDs, err := env.RepoIDs(1, org.NumRepos)
 	if err != nil {
-		return fmt.Errorf("GetUserRepositories [%d]: %w", userID, err)
+		return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err)
 	}
 	for _, repoID := range repoIDs {
-		if err = repo_model.WatchRepo(ctx, userID, repoID, false); err != nil {
+		repo, err := repo_model.GetRepositoryByID(ctx, repoID)
+		if err != nil {
+			return err
+		}
+		if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
 			return err
 		}
 	}
 
 	if len(repoIDs) > 0 {
 		if _, err = db.GetEngine(ctx).
-			Where("user_id = ?", userID).
+			Where("user_id = ?", user.ID).
 			In("repo_id", repoIDs).
 			Delete(new(access_model.Access)); err != nil {
 			return err
@@ -88,12 +88,12 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
 	}
 
 	// Delete member in their teams.
-	teams, err := organization.GetUserOrgTeams(ctx, org.ID, userID)
+	teams, err := organization.GetUserOrgTeams(ctx, org.ID, user.ID)
 	if err != nil {
 		return err
 	}
 	for _, t := range teams {
-		if err = removeTeamMember(ctx, t, userID); err != nil {
+		if err = removeTeamMember(ctx, t, user); err != nil {
 			return err
 		}
 	}
diff --git a/models/org_team.go b/models/org_team.go
index 1a452436c3..aecf0d80fd 100644
--- a/models/org_team.go
+++ b/models/org_team.go
@@ -44,7 +44,7 @@ func AddRepository(ctx context.Context, t *organization.Team, repo *repo_model.R
 			return fmt.Errorf("getMembers: %w", err)
 		}
 		for _, u := range t.Members {
-			if err = repo_model.WatchRepo(ctx, u.ID, repo.ID, true); err != nil {
+			if err = repo_model.WatchRepo(ctx, u, repo, true); err != nil {
 				return fmt.Errorf("watchRepo: %w", err)
 			}
 		}
@@ -125,7 +125,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error
 				continue
 			}
 
-			if err = repo_model.WatchRepo(ctx, user.ID, repo.ID, false); err != nil {
+			if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
 				return err
 			}
 
@@ -341,7 +341,7 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
 	}
 
 	for _, tm := range t.Members {
-		if err := removeInvalidOrgUser(ctx, tm.ID, t.OrgID); err != nil {
+		if err := removeInvalidOrgUser(ctx, t.OrgID, tm); err != nil {
 			return err
 		}
 	}
@@ -356,19 +356,23 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
 
 // AddTeamMember adds new membership of given team to given organization,
 // the user will have membership to given organization automatically when needed.
-func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
-	isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
+func AddTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
+	if user_model.IsUserBlockedBy(ctx, user, team.OrgID) {
+		return user_model.ErrBlockedUser
+	}
+
+	isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
 	if err != nil || isAlreadyMember {
 		return err
 	}
 
-	if err := organization.AddOrgUser(ctx, team.OrgID, userID); err != nil {
+	if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil {
 		return err
 	}
 
 	err = db.WithTx(ctx, func(ctx context.Context) error {
 		// check in transaction
-		isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
+		isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
 		if err != nil || isAlreadyMember {
 			return err
 		}
@@ -376,7 +380,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 		sess := db.GetEngine(ctx)
 
 		if err := db.Insert(ctx, &organization.TeamUser{
-			UID:    userID,
+			UID:    user.ID,
 			OrgID:  team.OrgID,
 			TeamID: team.ID,
 		}); err != nil {
@@ -392,7 +396,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 		subQuery := builder.Select("repo_id").From("team_repo").
 			Where(builder.Eq{"team_id": team.ID})
 
-		if _, err := sess.Where("user_id=?", userID).
+		if _, err := sess.Where("user_id=?", user.ID).
 			In("repo_id", subQuery).
 			And("mode < ?", team.AccessMode).
 			SetExpr("mode", team.AccessMode).
@@ -402,14 +406,14 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 
 		// for not exist access
 		var repoIDs []int64
-		accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": userID})
+		accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": user.ID})
 		if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil {
 			return fmt.Errorf("select id accesses: %w", err)
 		}
 
 		accesses := make([]*access_model.Access, 0, 100)
 		for i, repoID := range repoIDs {
-			accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: userID, Mode: team.AccessMode})
+			accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: user.ID, Mode: team.AccessMode})
 			if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 {
 				if err = db.Insert(ctx, accesses); err != nil {
 					return fmt.Errorf("insert new user accesses: %w", err)
@@ -430,10 +434,11 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 		if err := team.LoadRepositories(ctx); err != nil {
 			log.Error("team.LoadRepositories failed: %v", err)
 		}
+
 		// FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment
 		go func(repos []*repo_model.Repository) {
 			for _, repo := range repos {
-				if err = repo_model.WatchRepo(db.DefaultContext, userID, repo.ID, true); err != nil {
+				if err = repo_model.WatchRepo(db.DefaultContext, user, repo, true); err != nil {
 					log.Error("watch repo failed: %v", err)
 				}
 			}
@@ -443,16 +448,16 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
 	return nil
 }
 
-func removeTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
+func removeTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
 	e := db.GetEngine(ctx)
-	isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
+	isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
 	if err != nil || !isMember {
 		return err
 	}
 
 	// Check if the user to delete is the last member in owner team.
 	if team.IsOwnerTeam() && team.NumMembers == 1 {
-		return organization.ErrLastOrgOwner{UID: userID}
+		return organization.ErrLastOrgOwner{UID: user.ID}
 	}
 
 	team.NumMembers--
@@ -462,7 +467,7 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
 	}
 
 	if _, err := e.Delete(&organization.TeamUser{
-		UID:    userID,
+		UID:    user.ID,
 		OrgID:  team.OrgID,
 		TeamID: team.ID,
 	}); err != nil {
@@ -476,76 +481,76 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
 
 	// Delete access to team repositories.
 	for _, repo := range team.Repos {
-		if err := access_model.RecalculateUserAccess(ctx, repo, userID); err != nil {
+		if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil {
 			return err
 		}
 
 		// Remove watches from now unaccessible
-		if err := ReconsiderWatches(ctx, repo, userID); err != nil {
+		if err := ReconsiderWatches(ctx, repo, user); err != nil {
 			return err
 		}
 
 		// Remove issue assignments from now unaccessible
-		if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil {
+		if err := ReconsiderRepoIssuesAssignee(ctx, repo, user); err != nil {
 			return err
 		}
 	}
 
-	return removeInvalidOrgUser(ctx, userID, team.OrgID)
+	return removeInvalidOrgUser(ctx, team.OrgID, user)
 }
 
-func removeInvalidOrgUser(ctx context.Context, userID, orgID int64) error {
+func removeInvalidOrgUser(ctx context.Context, orgID int64, user *user_model.User) error {
 	// Check if the user is a member of any team in the organization.
 	if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{
-		UID:   userID,
+		UID:   user.ID,
 		OrgID: orgID,
 	}); err != nil {
 		return err
 	} else if count == 0 {
-		return RemoveOrgUser(ctx, orgID, userID)
+		org, err := organization.GetOrgByID(ctx, orgID)
+		if err != nil {
+			return err
+		}
+
+		return RemoveOrgUser(ctx, org, user)
 	}
 	return nil
 }
 
 // RemoveTeamMember removes member from given team of given organization.
-func RemoveTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
+func RemoveTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
 	ctx, committer, err := db.TxContext(ctx)
 	if err != nil {
 		return err
 	}
 	defer committer.Close()
-	if err := removeTeamMember(ctx, team, userID); err != nil {
+	if err := removeTeamMember(ctx, team, user); err != nil {
 		return err
 	}
 	return committer.Commit()
 }
 
-func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error {
-	user, err := user_model.GetUserByID(ctx, uid)
-	if err != nil {
-		return err
-	}
-
+func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
 	if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned {
 		return err
 	}
 
-	if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}).
+	if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": user.ID}).
 		In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})).
 		Delete(&issues_model.IssueAssignees{}); err != nil {
-		return fmt.Errorf("Could not delete assignee[%d] %w", uid, err)
+		return fmt.Errorf("Could not delete assignee[%d] %w", user.ID, err)
 	}
 	return nil
 }
 
-func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error {
-	if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has {
+func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
+	if has, err := access_model.HasAccess(ctx, user.ID, repo); err != nil || has {
 		return err
 	}
-	if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
+	if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil {
 		return err
 	}
 
 	// Remove all IssueWatches a user has subscribed to in the repository
-	return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID)
+	return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID)
 }
diff --git a/models/org_team_test.go b/models/org_team_test.go
index e4b7b917e8..cf2c8be536 100644
--- a/models/org_team_test.go
+++ b/models/org_team_test.go
@@ -21,33 +21,42 @@ import (
 func TestTeam_AddMember(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	test := func(teamID, userID int64) {
-		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
-		assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID))
-		unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
-		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID})
+	test := func(team *organization.Team, user *user_model.User) {
+		assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
+		unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+		unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
 	}
-	test(1, 2)
-	test(1, 4)
-	test(3, 2)
+
+	team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+	team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+	test(team1, user2)
+	test(team1, user4)
+	test(team3, user2)
 }
 
 func TestTeam_RemoveMember(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	testSuccess := func(teamID, userID int64) {
-		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
-		assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID))
-		unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
-		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID})
+	testSuccess := func(team *organization.Team, user *user_model.User) {
+		assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
+		unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+		unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
 	}
-	testSuccess(1, 4)
-	testSuccess(2, 2)
-	testSuccess(3, 2)
-	testSuccess(3, unittest.NonexistentID)
 
-	team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
-	err := RemoveTeamMember(db.DefaultContext, team, 2)
+	team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+	team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+	team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+	testSuccess(team1, user4)
+	testSuccess(team2, user2)
+	testSuccess(team3, user2)
+
+	err := RemoveTeamMember(db.DefaultContext, team1, user2)
 	assert.True(t, organization.IsErrLastOrgOwner(err))
 }
 
@@ -120,33 +129,42 @@ func TestDeleteTeam(t *testing.T) {
 func TestAddTeamMember(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	test := func(teamID, userID int64) {
-		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
-		assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID))
-		unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
-		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID})
+	test := func(team *organization.Team, user *user_model.User) {
+		assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
+		unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+		unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
 	}
-	test(1, 2)
-	test(1, 4)
-	test(3, 2)
+
+	team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+	team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+	test(team1, user2)
+	test(team1, user4)
+	test(team3, user2)
 }
 
 func TestRemoveTeamMember(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	testSuccess := func(teamID, userID int64) {
-		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
-		assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID))
-		unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
-		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID})
+	testSuccess := func(team *organization.Team, user *user_model.User) {
+		assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
+		unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+		unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
 	}
-	testSuccess(1, 4)
-	testSuccess(2, 2)
-	testSuccess(3, 2)
-	testSuccess(3, unittest.NonexistentID)
 
-	team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
-	err := RemoveTeamMember(db.DefaultContext, team, 2)
+	team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+	team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+	team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+	testSuccess(team1, user4)
+	testSuccess(team2, user2)
+	testSuccess(team3, user2)
+
+	err := RemoveTeamMember(db.DefaultContext, team1, user2)
 	assert.True(t, organization.IsErrLastOrgOwner(err))
 }
 
@@ -155,15 +173,15 @@ func TestRepository_RecalculateAccesses3(t *testing.T) {
 	team5 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5})
 	user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
 
-	has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23})
+	has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
 	assert.NoError(t, err)
 	assert.False(t, has)
 
 	// adding user29 to team5 should add an explicit access row for repo 23
 	// even though repo 23 is public
-	assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29.ID))
+	assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29))
 
-	has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23})
+	has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
 	assert.NoError(t, err)
 	assert.True(t, has)
 }
diff --git a/models/org_test.go b/models/org_test.go
index d10a1dc218..247530406d 100644
--- a/models/org_test.go
+++ b/models/org_test.go
@@ -16,22 +16,27 @@ import (
 
 func TestUser_RemoveMember(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
+
 	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+	user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 
 	// remove a user that is a member
-	unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: 4, OrgID: 3})
+	unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
 	prevNumMembers := org.NumMembers
-	assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 4))
-	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 4, OrgID: 3})
-	org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user4))
+	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
+
+	org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
 	assert.Equal(t, prevNumMembers-1, org.NumMembers)
 
 	// remove a user that is not a member
-	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3})
+	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
 	prevNumMembers = org.NumMembers
-	assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 5))
-	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3})
-	org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user5))
+	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
+
+	org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
 	assert.Equal(t, prevNumMembers, org.NumMembers)
 
 	unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
@@ -39,23 +44,31 @@ func TestUser_RemoveMember(t *testing.T) {
 
 func TestRemoveOrgUser(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	testSuccess := func(orgID, userID int64) {
-		org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
+
+	testSuccess := func(org *organization.Organization, user *user_model.User) {
 		expectedNumMembers := org.NumMembers
-		if unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) {
+		if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) {
 			expectedNumMembers--
 		}
-		assert.NoError(t, RemoveOrgUser(db.DefaultContext, orgID, userID))
-		unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: orgID, UID: userID})
-		org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
+		assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user))
+		unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID})
+		org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
 		assert.EqualValues(t, expectedNumMembers, org.NumMembers)
 	}
-	testSuccess(3, 4)
-	testSuccess(3, 4)
 
-	err := RemoveOrgUser(db.DefaultContext, 7, 5)
+	org3 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	org7 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 7})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+	user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+
+	testSuccess(org3, user4)
+
+	org3 = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	testSuccess(org3, user4)
+
+	err := RemoveOrgUser(db.DefaultContext, org7, user5)
 	assert.Error(t, err)
 	assert.True(t, organization.IsErrLastOrgOwner(err))
-	unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: 7, UID: 5})
+	unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: org7.ID, UID: user5.ID})
 	unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
 }
diff --git a/models/organization/org.go b/models/organization/org.go
index 23a4e2f96a..b33d15d29c 100644
--- a/models/organization/org.go
+++ b/models/organization/org.go
@@ -9,6 +9,7 @@ import (
 	"fmt"
 	"strings"
 
+	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -319,8 +320,9 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode
 
 	// Add initial creator to organization and owner team.
 	if err = db.Insert(ctx, &OrgUser{
-		UID:   owner.ID,
-		OrgID: org.ID,
+		UID:      owner.ID,
+		OrgID:    org.ID,
+		IsPublic: setting.Service.DefaultOrgMemberVisible,
 	}); err != nil {
 		return fmt.Errorf("insert org-user relation: %w", err)
 	}
@@ -400,6 +402,9 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
 		&TeamUnit{OrgID: org.ID},
 		&TeamInvite{OrgID: org.ID},
 		&secret_model.Secret{OwnerID: org.ID},
+		&user_model.Blocking{BlockerID: org.ID},
+		&actions_model.ActionRunner{OwnerID: org.ID},
+		&actions_model.ActionRunnerToken{OwnerID: org.ID},
 	); err != nil {
 		return fmt.Errorf("DeleteBeans: %w", err)
 	}
@@ -594,9 +599,7 @@ func GetOrgByID(ctx context.Context, id int64) (*Organization, error) {
 		return nil, err
 	} else if !has {
 		return nil, user_model.ErrUserNotExist{
-			UID:   id,
-			Name:  "",
-			KeyID: 0,
+			UID: id,
 		}
 	}
 	return u, nil
diff --git a/models/organization/team_user.go b/models/organization/team_user.go
index ab767db200..d6d0a5054d 100644
--- a/models/organization/team_user.go
+++ b/models/organization/team_user.go
@@ -30,14 +30,6 @@ func IsTeamMember(ctx context.Context, orgID, teamID, userID int64) (bool, error
 		Exist()
 }
 
-// GetTeamUsersByTeamID returns team users for a team
-func GetTeamUsersByTeamID(ctx context.Context, teamID int64) ([]*TeamUser, error) {
-	teamUsers := make([]*TeamUser, 0, 10)
-	return teamUsers, db.GetEngine(ctx).
-		Where("team_id=?", teamID).
-		Find(&teamUsers)
-}
-
 // SearchMembersOptions holds the search options
 type SearchMembersOptions struct {
 	db.ListOptions
diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go
index f849ab5c04..b8ef698d38 100644
--- a/models/packages/descriptor.go
+++ b/models/packages/descriptor.go
@@ -70,16 +70,26 @@ type PackageFileDescriptor struct {
 	Properties PackagePropertyList
 }
 
-// PackageWebLink returns the package web link
+// PackageWebLink returns the relative package web link
 func (pd *PackageDescriptor) PackageWebLink() string {
 	return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName))
 }
 
-// FullWebLink returns the package version web link
-func (pd *PackageDescriptor) FullWebLink() string {
+// VersionWebLink returns the relative package version web link
+func (pd *PackageDescriptor) VersionWebLink() string {
 	return fmt.Sprintf("%s/%s", pd.PackageWebLink(), url.PathEscape(pd.Version.LowerVersion))
 }
 
+// PackageHTMLURL returns the absolute package HTML URL
+func (pd *PackageDescriptor) PackageHTMLURL() string {
+	return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HTMLURL(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName))
+}
+
+// VersionHTMLURL returns the absolute package version HTML URL
+func (pd *PackageDescriptor) VersionHTMLURL() string {
+	return fmt.Sprintf("%s/%s", pd.PackageHTMLURL(), url.PathEscape(pd.Version.LowerVersion))
+}
+
 // CalculateBlobSize returns the total blobs size in bytes
 func (pd *PackageDescriptor) CalculateBlobSize() int64 {
 	size := int64(0)
diff --git a/models/packages/nuget/search.go b/models/packages/nuget/search.go
index 53cdf2d4ad..7a505ff08f 100644
--- a/models/packages/nuget/search.go
+++ b/models/packages/nuget/search.go
@@ -55,7 +55,7 @@ func CountPackages(ctx context.Context, opts *packages_model.PackageSearchOption
 
 func toConds(opts *packages_model.PackageSearchOptions) builder.Cond {
 	var cond builder.Cond = builder.Eq{
-		"package.is_internal": opts.IsInternal.IsTrue(),
+		"package.is_internal": opts.IsInternal.Value(),
 		"package.owner_id":    opts.OwnerID,
 		"package.type":        packages_model.TypeNuGet,
 	}
diff --git a/models/packages/package_version.go b/models/packages/package_version.go
index 8fc475691b..505dbaa0a5 100644
--- a/models/packages/package_version.go
+++ b/models/packages/package_version.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
@@ -105,7 +106,7 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType
 			ExactMatch: true,
 			Value:      version,
 		},
-		IsInternal: util.OptionalBoolOf(isInternal),
+		IsInternal: optional.Some(isInternal),
 		Paginator:  db.NewAbsoluteListOptions(0, 1),
 	})
 	if err != nil {
@@ -122,7 +123,7 @@ func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Ty
 	pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{
 		OwnerID:    ownerID,
 		Type:       packageType,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	return pvs, err
 }
@@ -136,7 +137,7 @@ func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Ty
 			ExactMatch: true,
 			Value:      name,
 		},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	return pvs, err
 }
@@ -182,18 +183,18 @@ type PackageSearchOptions struct {
 	Name            SearchValue       // only results with the specific name are found
 	Version         SearchValue       // only results with the specific version are found
 	Properties      map[string]string // only results are found which contain all listed version properties with the specific value
-	IsInternal      util.OptionalBool
-	HasFileWithName string            // only results are found which are associated with a file with the specific name
-	HasFiles        util.OptionalBool // only results are found which have associated files
+	IsInternal      optional.Option[bool]
+	HasFileWithName string                // only results are found which are associated with a file with the specific name
+	HasFiles        optional.Option[bool] // only results are found which have associated files
 	Sort            VersionSort
 	db.Paginator
 }
 
 func (opts *PackageSearchOptions) ToConds() builder.Cond {
 	cond := builder.NewCond()
-	if !opts.IsInternal.IsNone() {
+	if opts.IsInternal.Has() {
 		cond = builder.Eq{
-			"package_version.is_internal": opts.IsInternal.IsTrue(),
+			"package_version.is_internal": opts.IsInternal.Value(),
 		}
 	}
 
@@ -250,10 +251,10 @@ func (opts *PackageSearchOptions) ToConds() builder.Cond {
 		cond = cond.And(builder.Exists(builder.Select("package_file.id").From("package_file").Where(fileCond)))
 	}
 
-	if !opts.HasFiles.IsNone() {
+	if opts.HasFiles.Has() {
 		filesCond := builder.Exists(builder.Select("package_file.id").From("package_file").Where(builder.Expr("package_file.version_id = package_version.id")))
 
-		if opts.HasFiles.IsFalse() {
+		if !opts.HasFiles.Value() {
 			filesCond = builder.Not{filesCond}
 		}
 
@@ -307,8 +308,8 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
 		And(builder.Expr("pv2.id IS NULL"))
 
 	joinCond := builder.Expr("package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))")
-	if !opts.IsInternal.IsNone() {
-		joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.IsTrue()})
+	if opts.IsInternal.Has() {
+		joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.Value()})
 	}
 
 	sess := db.GetEngine(ctx).
diff --git a/models/perm/access/access.go b/models/perm/access/access.go
index 3e2568b4b4..b422a08614 100644
--- a/models/perm/access/access.go
+++ b/models/perm/access/access.go
@@ -128,9 +128,9 @@ func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap
 
 // refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
 func refreshCollaboratorAccesses(ctx context.Context, repoID int64, accessMap map[int64]*userAccess) error {
-	collaborators, err := repo_model.GetCollaborators(ctx, repoID, db.ListOptions{})
+	collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repoID})
 	if err != nil {
-		return fmt.Errorf("getCollaborations: %w", err)
+		return fmt.Errorf("GetCollaborators: %w", err)
 	}
 	for _, c := range collaborators {
 		if c.User.IsGhost() {
diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go
index 395ecdf1a5..4175cb9b92 100644
--- a/models/perm/access/repo_permission.go
+++ b/models/perm/access/repo_permission.go
@@ -332,7 +332,6 @@ func HasAccessUnit(ctx context.Context, user *user_model.User, repo *repo_model.
 
 // CanBeAssigned return true if user can be assigned to issue or pull requests in repo
 // Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface.
-// FIXME: user could send PullRequest also could be assigned???
 func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository, _ bool) (bool, error) {
 	if user.IsOrganization() {
 		return false, fmt.Errorf("Organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID)
@@ -341,7 +340,8 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.
 	if err != nil {
 		return false, err
 	}
-	return perm.CanAccessAny(perm_model.AccessModeWrite, unit.TypeCode, unit.TypeIssues, unit.TypePullRequests), nil
+	return perm.CanAccessAny(perm_model.AccessModeWrite, unit.AllRepoUnitTypes...) ||
+		perm.CanAccessAny(perm_model.AccessModeRead, unit.TypePullRequests), nil
 }
 
 // HasAccess returns true if user has access to repo
diff --git a/models/project/board.go b/models/project/board.go
index 3e2d8e0472..5f142a356c 100644
--- a/models/project/board.go
+++ b/models/project/board.go
@@ -123,6 +123,17 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error {
 		return nil
 	}
 
+	board := Board{
+		CreatedUnix: timeutil.TimeStampNow(),
+		CreatorID:   project.CreatorID,
+		Title:       "Backlog",
+		ProjectID:   project.ID,
+		Default:     true,
+	}
+	if err := db.Insert(ctx, board); err != nil {
+		return err
+	}
+
 	if len(items) == 0 {
 		return nil
 	}
@@ -176,6 +187,10 @@ func deleteBoardByID(ctx context.Context, boardID int64) error {
 		return err
 	}
 
+	if board.Default {
+		return fmt.Errorf("deleteBoardByID: cannot delete default board")
+	}
+
 	if err = board.removeIssues(ctx); err != nil {
 		return err
 	}
@@ -194,7 +209,6 @@ func deleteBoardByProjectID(ctx context.Context, projectID int64) error {
 // GetBoard fetches the current board of a project
 func GetBoard(ctx context.Context, boardID int64) (*Board, error) {
 	board := new(Board)
-
 	has, err := db.GetEngine(ctx).ID(boardID).Get(board)
 	if err != nil {
 		return nil, err
@@ -228,11 +242,10 @@ func UpdateBoard(ctx context.Context, board *Board) error {
 }
 
 // GetBoards fetches all boards related to a project
-// if no default board set, first board is a temporary "Uncategorized" board
 func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
 	boards := make([]*Board, 0, 5)
 
-	if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("Sorting").Find(&boards); err != nil {
+	if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("sorting").Find(&boards); err != nil {
 		return nil, err
 	}
 
@@ -244,53 +257,64 @@ func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
 	return append([]*Board{defaultB}, boards...), nil
 }
 
-// getDefaultBoard return default board and create a dummy if none exist
+// getDefaultBoard return default board and ensure only one exists
 func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) {
 	var board Board
-	exist, err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, true).Get(&board)
+	has, err := db.GetEngine(ctx).
+		Where("project_id=? AND `default` = ?", p.ID, true).
+		Desc("id").Get(&board)
 	if err != nil {
 		return nil, err
 	}
-	if exist {
+
+	if has {
 		return &board, nil
 	}
 
-	// represents a board for issues not assigned to one
-	return &Board{
+	// create a default board if none is found
+	board = Board{
 		ProjectID: p.ID,
-		Title:     "Uncategorized",
 		Default:   true,
-	}, nil
+		Title:     "Uncategorized",
+		CreatorID: p.CreatorID,
+	}
+	if _, err := db.GetEngine(ctx).Insert(&board); err != nil {
+		return nil, err
+	}
+	return &board, nil
 }
 
 // SetDefaultBoard represents a board for issues not assigned to one
-// if boardID is 0 unset default
 func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error {
-	_, err := db.GetEngine(ctx).Where(builder.Eq{
-		"project_id": projectID,
-		"`default`":  true,
-	}).Cols("`default`").Update(&Board{Default: false})
-	if err != nil {
-		return err
-	}
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		if _, err := GetBoard(ctx, boardID); err != nil {
+			return err
+		}
 
-	if boardID > 0 {
-		_, err = db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}).
+		if _, err := db.GetEngine(ctx).Where(builder.Eq{
+			"project_id": projectID,
+			"`default`":  true,
+		}).Cols("`default`").Update(&Board{Default: false}); err != nil {
+			return err
+		}
+
+		_, err := db.GetEngine(ctx).ID(boardID).
+			Where(builder.Eq{"project_id": projectID}).
 			Cols("`default`").Update(&Board{Default: true})
-	}
-
-	return err
+		return err
+	})
 }
 
 // UpdateBoardSorting update project board sorting
 func UpdateBoardSorting(ctx context.Context, bs BoardList) error {
-	for i := range bs {
-		_, err := db.GetEngine(ctx).ID(bs[i].ID).Cols(
-			"sorting",
-		).Update(bs[i])
-		if err != nil {
-			return err
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		for i := range bs {
+			if _, err := db.GetEngine(ctx).ID(bs[i].ID).Cols(
+				"sorting",
+			).Update(bs[i]); err != nil {
+				return err
+			}
 		}
-	}
-	return nil
+		return nil
+	})
 }
diff --git a/models/project/board_test.go b/models/project/board_test.go
new file mode 100644
index 0000000000..71ba29a589
--- /dev/null
+++ b/models/project/board_test.go
@@ -0,0 +1,44 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package project
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unittest"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetDefaultBoard(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5)
+	assert.NoError(t, err)
+
+	// check if default board was added
+	board, err := projectWithoutDefault.getDefaultBoard(db.DefaultContext)
+	assert.NoError(t, err)
+	assert.Equal(t, int64(5), board.ProjectID)
+	assert.Equal(t, "Uncategorized", board.Title)
+
+	projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
+	assert.NoError(t, err)
+
+	// check if multiple defaults were removed
+	board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext)
+	assert.NoError(t, err)
+	assert.Equal(t, int64(6), board.ProjectID)
+	assert.Equal(t, int64(9), board.ID)
+
+	// set 8 as default board
+	assert.NoError(t, SetDefaultBoard(db.DefaultContext, board.ProjectID, 8))
+
+	// then 9 will become a non-default board
+	board, err = GetBoard(db.DefaultContext, 9)
+	assert.NoError(t, err)
+	assert.Equal(t, int64(6), board.ProjectID)
+	assert.False(t, board.Default)
+}
diff --git a/models/project/project.go b/models/project/project.go
index d2fca6cdc8..8f9ee2a99e 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -6,11 +6,13 @@ package project
 import (
 	"context"
 	"fmt"
+	"html/template"
 
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -100,7 +102,7 @@ type Project struct {
 	CardType    CardType
 	Type        Type
 
-	RenderedContent string `xorm:"-"`
+	RenderedContent template.HTML `xorm:"-"`
 
 	CreatedUnix    timeutil.TimeStamp `xorm:"INDEX created"`
 	UpdatedUnix    timeutil.TimeStamp `xorm:"INDEX updated"`
@@ -195,7 +197,7 @@ type SearchOptions struct {
 	db.ListOptions
 	OwnerID  int64
 	RepoID   int64
-	IsClosed util.OptionalBool
+	IsClosed optional.Option[bool]
 	OrderBy  db.SearchOrderBy
 	Type     Type
 	Title    string
@@ -206,11 +208,8 @@ func (opts SearchOptions) ToConds() builder.Cond {
 	if opts.RepoID > 0 {
 		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
 	}
-	switch opts.IsClosed {
-	case util.OptionalBoolTrue:
-		cond = cond.And(builder.Eq{"is_closed": true})
-	case util.OptionalBoolFalse:
-		cond = cond.And(builder.Eq{"is_closed": false})
+	if opts.IsClosed.Has() {
+		cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()})
 	}
 
 	if opts.Type > 0 {
diff --git a/models/project/project_test.go b/models/project/project_test.go
index 7a37c1faf2..8fbbdedecf 100644
--- a/models/project/project_test.go
+++ b/models/project/project_test.go
@@ -92,19 +92,19 @@ func TestProjectsSort(t *testing.T) {
 	}{
 		{
 			sortType: "default",
-			wants:    []int64{1, 3, 2, 4},
+			wants:    []int64{1, 3, 2, 6, 5, 4},
 		},
 		{
 			sortType: "oldest",
-			wants:    []int64{4, 2, 3, 1},
+			wants:    []int64{4, 5, 6, 2, 3, 1},
 		},
 		{
 			sortType: "recentupdate",
-			wants:    []int64{1, 3, 2, 4},
+			wants:    []int64{1, 3, 2, 6, 5, 4},
 		},
 		{
 			sortType: "leastupdate",
-			wants:    []int64{4, 2, 3, 1},
+			wants:    []int64{4, 5, 6, 2, 3, 1},
 		},
 	}
 
@@ -113,8 +113,8 @@ func TestProjectsSort(t *testing.T) {
 			OrderBy: GetSearchOrderByBySortType(tt.sortType),
 		})
 		assert.NoError(t, err)
-		assert.EqualValues(t, int64(4), count)
-		if assert.Len(t, projects, 4) {
+		assert.EqualValues(t, int64(6), count)
+		if assert.Len(t, projects, 6) {
 			for i := range projects {
 				assert.EqualValues(t, tt.wants[i], projects[i].ID)
 			}
diff --git a/models/repo/attachment.go b/models/repo/attachment.go
index 1a588398c1..9b0de11fdc 100644
--- a/models/repo/attachment.go
+++ b/models/repo/attachment.go
@@ -24,7 +24,7 @@ type Attachment struct {
 	IssueID           int64  `xorm:"INDEX"`           // maybe zero when creating
 	ReleaseID         int64  `xorm:"INDEX"`           // maybe zero when creating
 	UploaderID        int64  `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added
-	CommentID         int64
+	CommentID         int64  `xorm:"INDEX"`
 	Name              string
 	DownloadCount     int64              `xorm:"DEFAULT 0"`
 	Size              int64              `xorm:"DEFAULT 0"`
diff --git a/models/repo/collaboration.go b/models/repo/collaboration.go
index 7288082614..272c6ac05b 100644
--- a/models/repo/collaboration.go
+++ b/models/repo/collaboration.go
@@ -36,14 +36,44 @@ type Collaborator struct {
 	Collaboration *Collaboration
 }
 
+type FindCollaborationOptions struct {
+	db.ListOptions
+	RepoID         int64
+	RepoOwnerID    int64
+	CollaboratorID int64
+}
+
+func (opts *FindCollaborationOptions) ToConds() builder.Cond {
+	cond := builder.NewCond()
+	if opts.RepoID != 0 {
+		cond = cond.And(builder.Eq{"collaboration.repo_id": opts.RepoID})
+	}
+	if opts.RepoOwnerID != 0 {
+		cond = cond.And(builder.Eq{"repository.owner_id": opts.RepoOwnerID})
+	}
+	if opts.CollaboratorID != 0 {
+		cond = cond.And(builder.Eq{"collaboration.user_id": opts.CollaboratorID})
+	}
+	return cond
+}
+
+func (opts *FindCollaborationOptions) ToJoins() []db.JoinFunc {
+	if opts.RepoOwnerID != 0 {
+		return []db.JoinFunc{
+			func(e db.Engine) error {
+				e.Join("INNER", "repository", "repository.id = collaboration.repo_id")
+				return nil
+			},
+		}
+	}
+	return nil
+}
+
 // GetCollaborators returns the collaborators for a repository
-func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*Collaborator, error) {
-	collaborations, err := db.Find[Collaboration](ctx, FindCollaborationOptions{
-		ListOptions: listOptions,
-		RepoID:      repoID,
-	})
+func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*Collaborator, int64, error) {
+	collaborations, total, err := db.FindAndCount[Collaboration](ctx, opts)
 	if err != nil {
-		return nil, fmt.Errorf("db.Find[Collaboration]: %w", err)
+		return nil, 0, fmt.Errorf("db.FindAndCount[Collaboration]: %w", err)
 	}
 
 	collaborators := make([]*Collaborator, 0, len(collaborations))
@@ -54,7 +84,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
 
 	usersMap := make(map[int64]*user_model.User)
 	if err := db.GetEngine(ctx).In("id", userIDs).Find(&usersMap); err != nil {
-		return nil, fmt.Errorf("Find users map by user ids: %w", err)
+		return nil, 0, fmt.Errorf("Find users map by user ids: %w", err)
 	}
 
 	for _, c := range collaborations {
@@ -67,7 +97,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
 			Collaboration: c,
 		})
 	}
-	return collaborators, nil
+	return collaborators, total, nil
 }
 
 // GetCollaboration get collaboration for a repository id with a user id
@@ -88,15 +118,6 @@ func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
 	return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
 }
 
-type FindCollaborationOptions struct {
-	db.ListOptions
-	RepoID int64
-}
-
-func (opts FindCollaborationOptions) ToConds() builder.Cond {
-	return builder.And(builder.Eq{"repo_id": opts.RepoID})
-}
-
 // ChangeCollaborationAccessMode sets new access mode for the collaboration.
 func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid int64, mode perm.AccessMode) error {
 	// Discard invalid input
diff --git a/models/repo/collaboration_test.go b/models/repo/collaboration_test.go
index 21a99dd557..639050f5fd 100644
--- a/models/repo/collaboration_test.go
+++ b/models/repo/collaboration_test.go
@@ -19,7 +19,7 @@ func TestRepository_GetCollaborators(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	test := func(repoID int64) {
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
-		collaborators, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{})
+		collaborators, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
 		assert.NoError(t, err)
 		expectedLen, err := db.GetEngine(db.DefaultContext).Count(&repo_model.Collaboration{RepoID: repoID})
 		assert.NoError(t, err)
@@ -37,11 +37,17 @@ func TestRepository_GetCollaborators(t *testing.T) {
 	// Test db.ListOptions
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
 
-	collaborators1, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 1})
+	collaborators1, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
+		ListOptions: db.ListOptions{PageSize: 1, Page: 1},
+		RepoID:      repo.ID,
+	})
 	assert.NoError(t, err)
 	assert.Len(t, collaborators1, 1)
 
-	collaborators2, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 2})
+	collaborators2, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
+		ListOptions: db.ListOptions{PageSize: 1, Page: 2},
+		RepoID:      repo.ID,
+	})
 	assert.NoError(t, err)
 	assert.Len(t, collaborators2, 1)
 
@@ -85,31 +91,6 @@ func TestRepository_ChangeCollaborationAccessMode(t *testing.T) {
 	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 }
 
-func TestRepository_CountCollaborators(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
-	count, err := db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
-		RepoID: repo1.ID,
-	})
-	assert.NoError(t, err)
-	assert.EqualValues(t, 2, count)
-
-	repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
-	count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
-		RepoID: repo2.ID,
-	})
-	assert.NoError(t, err)
-	assert.EqualValues(t, 2, count)
-
-	// Non-existent repository.
-	count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
-		RepoID: unittest.NonexistentID,
-	})
-	assert.NoError(t, err)
-	assert.EqualValues(t, 0, count)
-}
-
 func TestRepository_IsOwnerMemberCollaborator(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
diff --git a/models/repo/git.go b/models/repo/git.go
index 610c554296..388bf86522 100644
--- a/models/repo/git.go
+++ b/models/repo/git.go
@@ -21,6 +21,8 @@ const (
 	MergeStyleRebaseMerge MergeStyle = "rebase-merge"
 	// MergeStyleSquash squash commits into single commit before merging
 	MergeStyleSquash MergeStyle = "squash"
+	// MergeStyleFastForwardOnly fast-forward merge if possible, otherwise fail
+	MergeStyleFastForwardOnly MergeStyle = "fast-forward-only"
 	// MergeStyleManuallyMerged pr has been merged manually, just mark it as merged directly
 	MergeStyleManuallyMerged MergeStyle = "manually-merged"
 	// MergeStyleRebaseUpdate not a merge style, used to update pull head by rebase
diff --git a/models/repo/issue.go b/models/repo/issue.go
index 6f6b565a00..0dd4fd5ed4 100644
--- a/models/repo/issue.go
+++ b/models/repo/issue.go
@@ -53,7 +53,7 @@ func (repo *Repository) IsDependenciesEnabled(ctx context.Context) bool {
 	var u *RepoUnit
 	var err error
 	if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
-		log.Trace("%s", err)
+		log.Trace("IsDependenciesEnabled: %v", err)
 		return setting.Service.DefaultEnableDependencies
 	}
 	return u.IssuesConfig().EnableDependencies
diff --git a/models/repo/release.go b/models/repo/release.go
index 1f37f11b2e..a9f65f6c3e 100644
--- a/models/repo/release.go
+++ b/models/repo/release.go
@@ -7,6 +7,7 @@ package repo
 import (
 	"context"
 	"fmt"
+	"html/template"
 	"net/url"
 	"sort"
 	"strconv"
@@ -15,6 +16,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -79,7 +81,7 @@ type Release struct {
 	NumCommits       int64
 	NumCommitsBehind int64              `xorm:"-"`
 	Note             string             `xorm:"TEXT"`
-	RenderedNote     string             `xorm:"-"`
+	RenderedNote     template.HTML      `xorm:"-"`
 	IsDraft          bool               `xorm:"NOT NULL DEFAULT false"`
 	IsPrerelease     bool               `xorm:"NOT NULL DEFAULT false"`
 	IsTag            bool               `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
@@ -228,10 +230,10 @@ type FindReleasesOptions struct {
 	RepoID        int64
 	IncludeDrafts bool
 	IncludeTags   bool
-	IsPreRelease  util.OptionalBool
-	IsDraft       util.OptionalBool
+	IsPreRelease  optional.Option[bool]
+	IsDraft       optional.Option[bool]
 	TagNames      []string
-	HasSha1       util.OptionalBool // useful to find draft releases which are created with existing tags
+	HasSha1       optional.Option[bool] // useful to find draft releases which are created with existing tags
 }
 
 func (opts FindReleasesOptions) ToConds() builder.Cond {
@@ -246,14 +248,14 @@ func (opts FindReleasesOptions) ToConds() builder.Cond {
 	if len(opts.TagNames) > 0 {
 		cond = cond.And(builder.In("tag_name", opts.TagNames))
 	}
-	if !opts.IsPreRelease.IsNone() {
-		cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.IsTrue()})
+	if opts.IsPreRelease.Has() {
+		cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.Value()})
 	}
-	if !opts.IsDraft.IsNone() {
-		cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.IsTrue()})
+	if opts.IsDraft.Has() {
+		cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.Value()})
 	}
-	if !opts.HasSha1.IsNone() {
-		if opts.HasSha1.IsTrue() {
+	if opts.HasSha1.Has() {
+		if opts.HasSha1.Value() {
 			cond = cond.And(builder.Neq{"sha1": ""})
 		} else {
 			cond = cond.And(builder.Eq{"sha1": ""})
@@ -275,7 +277,7 @@ func GetTagNamesByRepoID(ctx context.Context, repoID int64) ([]string, error) {
 		ListOptions:   listOptions,
 		IncludeDrafts: true,
 		IncludeTags:   true,
-		HasSha1:       util.OptionalBoolTrue,
+		HasSha1:       optional.Some(true),
 		RepoID:        repoID,
 	}
 
diff --git a/models/repo/repo.go b/models/repo/repo.go
index 13493ba6e8..5d5707d1ac 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -135,6 +136,7 @@ type Repository struct {
 	OriginalServiceType api.GitServiceType `xorm:"index"`
 	OriginalURL         string             `xorm:"VARCHAR(2048)"`
 	DefaultBranch       string
+	DefaultWikiBranch   string
 
 	NumWatches          int
 	NumStars            int
@@ -284,6 +286,9 @@ func (repo *Repository) AfterLoad() {
 	repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
 	repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects
 	repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns
+	if repo.DefaultWikiBranch == "" {
+		repo.DefaultWikiBranch = setting.Repository.DefaultBranch
+	}
 }
 
 // LoadAttributes loads attributes of the repository.
@@ -410,6 +415,13 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit
 			Type:   tp,
 			Config: new(ActionsConfig),
 		}
+	} else if tp == unit.TypeProjects {
+		cfg := new(ProjectsConfig)
+		cfg.ProjectsMode = ProjectsModeNone
+		return &RepoUnit{
+			Type:   tp,
+			Config: cfg,
+		}
 	}
 
 	return &RepoUnit{
@@ -519,6 +531,9 @@ func (repo *Repository) GetBaseRepo(ctx context.Context) (err error) {
 		return nil
 	}
 
+	if repo.BaseRepo != nil {
+		return nil
+	}
 	repo.BaseRepo, err = GetRepositoryByID(ctx, repo.ForkID)
 	return err
 }
@@ -840,7 +855,7 @@ func (repo *Repository) TemplateRepo(ctx context.Context) *Repository {
 
 type CountRepositoryOptions struct {
 	OwnerID int64
-	Private util.OptionalBool
+	Private optional.Option[bool]
 }
 
 // CountRepositories returns number of repositories.
@@ -852,8 +867,8 @@ func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64,
 	if opts.OwnerID > 0 {
 		sess.And("owner_id = ?", opts.OwnerID)
 	}
-	if !opts.Private.IsNone() {
-		sess.And("is_private=?", opts.Private.IsTrue())
+	if opts.Private.Has() {
+		sess.And("is_private=?", opts.Private.Value())
 	}
 
 	count, err := sess.Count(new(Repository))
diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go
index 533ca5251f..987c7df9b0 100644
--- a/models/repo/repo_list.go
+++ b/models/repo/repo_list.go
@@ -13,6 +13,7 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
@@ -62,24 +63,60 @@ func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList {
 	return RepositoryList(ValuesRepository(repoMap))
 }
 
+func (repos RepositoryList) LoadUnits(ctx context.Context) error {
+	if len(repos) == 0 {
+		return nil
+	}
+
+	// Load units.
+	units := make([]*RepoUnit, 0, len(repos)*6)
+	if err := db.GetEngine(ctx).
+		In("repo_id", repos.IDs()).
+		Find(&units); err != nil {
+		return fmt.Errorf("find units: %w", err)
+	}
+
+	unitsMap := make(map[int64][]*RepoUnit, len(repos))
+	for _, unit := range units {
+		if !unit.Type.UnitGlobalDisabled() {
+			unitsMap[unit.RepoID] = append(unitsMap[unit.RepoID], unit)
+		}
+	}
+
+	for _, repo := range repos {
+		repo.Units = unitsMap[repo.ID]
+	}
+
+	return nil
+}
+
+func (repos RepositoryList) IDs() []int64 {
+	repoIDs := make([]int64, len(repos))
+	for i := range repos {
+		repoIDs[i] = repos[i].ID
+	}
+	return repoIDs
+}
+
 // LoadAttributes loads the attributes for the given RepositoryList
 func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
 	if len(repos) == 0 {
 		return nil
 	}
 
-	set := make(container.Set[int64])
+	userIDs := container.FilterSlice(repos, func(repo *Repository) (int64, bool) {
+		return repo.OwnerID, true
+	})
 	repoIDs := make([]int64, len(repos))
 	for i := range repos {
-		set.Add(repos[i].OwnerID)
 		repoIDs[i] = repos[i].ID
 	}
 
 	// Load owners.
-	users := make(map[int64]*user_model.User, len(set))
+	users := make(map[int64]*user_model.User, len(userIDs))
 	if err := db.GetEngine(ctx).
 		Where("id > 0").
-		In("id", set.Values()).
+		In("id", userIDs).
 		Find(&users); err != nil {
 		return fmt.Errorf("find users: %w", err)
 	}
@@ -125,11 +162,11 @@ type SearchRepoOptions struct {
 	// None -> include public and private
 	// True -> include just private
 	// False -> include just public
-	IsPrivate util.OptionalBool
+	IsPrivate optional.Option[bool]
 	// None -> include collaborative AND non-collaborative
 	// True -> include just collaborative
 	// False -> include just non-collaborative
-	Collaborate util.OptionalBool
+	Collaborate optional.Option[bool]
 	// What type of unit the user can be collaborative in,
 	// it is ignored if Collaborate is False.
 	// TypeInvalid means any unit type.
@@ -137,19 +174,19 @@ type SearchRepoOptions struct {
 	// None -> include forks AND non-forks
 	// True -> include just forks
 	// False -> include just non-forks
-	Fork util.OptionalBool
+	Fork optional.Option[bool]
 	// None -> include templates AND non-templates
 	// True -> include just templates
 	// False -> include just non-templates
-	Template util.OptionalBool
+	Template optional.Option[bool]
 	// None -> include mirrors AND non-mirrors
 	// True -> include just mirrors
 	// False -> include just non-mirrors
-	Mirror util.OptionalBool
+	Mirror optional.Option[bool]
 	// None -> include archived AND non-archived
 	// True -> include just archived
 	// False -> include just non-archived
-	Archived util.OptionalBool
+	Archived optional.Option[bool]
 	// only search topic name
 	TopicOnly bool
 	// only search repositories with specified primary language
@@ -159,7 +196,7 @@ type SearchRepoOptions struct {
 	// None -> include has milestones AND has no milestone
 	// True -> include just has milestones
 	// False -> include just has no milestone
-	HasMilestones util.OptionalBool
+	HasMilestones optional.Option[bool]
 	// LowerNames represents valid lower names to restrict to
 	LowerNames []string
 	// When specified true, apply some filters over the conditions:
@@ -359,12 +396,12 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 			)))
 	}
 
-	if opts.IsPrivate != util.OptionalBoolNone {
-		cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.IsTrue()})
+	if opts.IsPrivate.Has() {
+		cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.Value()})
 	}
 
-	if opts.Template != util.OptionalBoolNone {
-		cond = cond.And(builder.Eq{"is_template": opts.Template == util.OptionalBoolTrue})
+	if opts.Template.Has() {
+		cond = cond.And(builder.Eq{"is_template": opts.Template.Value()})
 	}
 
 	// Restrict to starred repositories
@@ -380,11 +417,11 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 	// Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate
 	if opts.OwnerID > 0 {
 		accessCond := builder.NewCond()
-		if opts.Collaborate != util.OptionalBoolTrue {
+		if !opts.Collaborate.Value() {
 			accessCond = builder.Eq{"owner_id": opts.OwnerID}
 		}
 
-		if opts.Collaborate != util.OptionalBoolFalse {
+		if opts.Collaborate.ValueOrDefault(true) {
 			// A Collaboration is:
 
 			collaborateCond := builder.NewCond()
@@ -472,31 +509,32 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 			Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true})))
 	}
 
-	if opts.Fork != util.OptionalBoolNone || opts.OnlyShowRelevant {
-		if opts.OnlyShowRelevant && opts.Fork == util.OptionalBoolNone {
+	if opts.Fork.Has() || opts.OnlyShowRelevant {
+		if opts.OnlyShowRelevant && !opts.Fork.Has() {
 			cond = cond.And(builder.Eq{"is_fork": false})
 		} else {
-			cond = cond.And(builder.Eq{"is_fork": opts.Fork == util.OptionalBoolTrue})
+			cond = cond.And(builder.Eq{"is_fork": opts.Fork.Value()})
 		}
 	}
 
-	if opts.Mirror != util.OptionalBoolNone {
-		cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue})
+	if opts.Mirror.Has() {
+		cond = cond.And(builder.Eq{"is_mirror": opts.Mirror.Value()})
 	}
 
 	if opts.Actor != nil && opts.Actor.IsRestricted {
 		cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
 	}
 
-	if opts.Archived != util.OptionalBoolNone {
-		cond = cond.And(builder.Eq{"is_archived": opts.Archived == util.OptionalBoolTrue})
+	if opts.Archived.Has() {
+		cond = cond.And(builder.Eq{"is_archived": opts.Archived.Value()})
 	}
 
-	switch opts.HasMilestones {
-	case util.OptionalBoolTrue:
-		cond = cond.And(builder.Gt{"num_milestones": 0})
-	case util.OptionalBoolFalse:
-		cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"}))
+	if opts.HasMilestones.Has() {
+		if opts.HasMilestones.Value() {
+			cond = cond.And(builder.Gt{"num_milestones": 0})
+		} else {
+			cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"}))
+		}
 	}
 
 	if opts.OnlyShowRelevant {
diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go
index 8a1799aac0..88cfcde620 100644
--- a/models/repo/repo_list_test.go
+++ b/models/repo/repo_list_test.go
@@ -10,7 +10,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -27,62 +27,62 @@ func getTestCases() []struct {
 	}{
 		{
 			name:  "PublicRepositoriesByName",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: optional.Some(false)},
 			count: 7,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesByName",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: optional.Some(false)},
 			count: 14,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
 			count: 14,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
 			count: 14,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
 			count: 14,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)},
 			count: 14,
 		},
 		{
 			name:  "PublicRepositoriesOfUser",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: optional.Some(false)},
 			count: 2,
 		},
 		{
 			name:  "PublicRepositoriesOfUser2",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: optional.Some(false)},
 			count: 0,
 		},
 		{
 			name:  "PublicRepositoriesOfOrg3",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: optional.Some(false)},
 			count: 2,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesOfUser",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: optional.Some(false)},
 			count: 4,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesOfUser2",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: optional.Some(false)},
 			count: 0,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesOfOrg3",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: optional.Some(false)},
 			count: 4,
 		},
 		{
@@ -117,33 +117,33 @@ func getTestCases() []struct {
 		},
 		{
 			name:  "PublicRepositoriesOfOrganization",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: optional.Some(false)},
 			count: 1,
 		},
 		{
 			name:  "PublicAndPrivateRepositoriesOfOrganization",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: optional.Some(false)},
 			count: 2,
 		},
 		{
 			name:  "AllPublic/PublicRepositoriesByName",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: optional.Some(false)},
 			count: 7,
 		},
 		{
 			name:  "AllPublic/PublicAndPrivateRepositoriesByName",
-			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: util.OptionalBoolFalse},
+			opts:  &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: optional.Some(false)},
 			count: 14,
 		},
 		{
 			name:  "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse},
-			count: 31,
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)},
+			count: 33,
 		},
 		{
 			name:  "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse},
-			count: 36,
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)},
+			count: 38,
 		},
 		{
 			name:  "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
@@ -157,12 +157,12 @@ func getTestCases() []struct {
 		},
 		{
 			name:  "AllPublic/PublicRepositoriesOfOrganization",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse},
-			count: 31,
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)},
+			count: 33,
 		},
 		{
 			name:  "AllTemplates",
-			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: util.OptionalBoolTrue},
+			opts:  &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: optional.Some(true)},
 			count: 2,
 		},
 		{
@@ -190,7 +190,7 @@ func TestSearchRepository(t *testing.T) {
 			PageSize: 10,
 		},
 		Keyword:     "repo_12",
-		Collaborate: util.OptionalBoolFalse,
+		Collaborate: optional.Some(false),
 	})
 
 	assert.NoError(t, err)
@@ -205,7 +205,7 @@ func TestSearchRepository(t *testing.T) {
 			PageSize: 10,
 		},
 		Keyword:     "test_repo",
-		Collaborate: util.OptionalBoolFalse,
+		Collaborate: optional.Some(false),
 	})
 
 	assert.NoError(t, err)
@@ -220,7 +220,7 @@ func TestSearchRepository(t *testing.T) {
 		},
 		Keyword:     "repo_13",
 		Private:     true,
-		Collaborate: util.OptionalBoolFalse,
+		Collaborate: optional.Some(false),
 	})
 
 	assert.NoError(t, err)
@@ -236,7 +236,7 @@ func TestSearchRepository(t *testing.T) {
 		},
 		Keyword:     "test_repo",
 		Private:     true,
-		Collaborate: util.OptionalBoolFalse,
+		Collaborate: optional.Some(false),
 	})
 
 	assert.NoError(t, err)
@@ -257,7 +257,7 @@ func TestSearchRepository(t *testing.T) {
 			PageSize: 10,
 		},
 		Keyword:            "description_14",
-		Collaborate:        util.OptionalBoolFalse,
+		Collaborate:        optional.Some(false),
 		IncludeDescription: true,
 	})
 
@@ -274,7 +274,7 @@ func TestSearchRepository(t *testing.T) {
 			PageSize: 10,
 		},
 		Keyword:            "description_14",
-		Collaborate:        util.OptionalBoolFalse,
+		Collaborate:        optional.Some(false),
 		IncludeDescription: false,
 	})
 
@@ -327,30 +327,25 @@ func TestSearchRepository(t *testing.T) {
 						assert.False(t, repo.IsPrivate)
 					}
 
-					if testCase.opts.Fork == util.OptionalBoolTrue && testCase.opts.Mirror == util.OptionalBoolTrue {
-						assert.True(t, repo.IsFork || repo.IsMirror)
+					if testCase.opts.Fork.Value() && testCase.opts.Mirror.Value() {
+						assert.True(t, repo.IsFork && repo.IsMirror)
 					} else {
-						switch testCase.opts.Fork {
-						case util.OptionalBoolFalse:
-							assert.False(t, repo.IsFork)
-						case util.OptionalBoolTrue:
-							assert.True(t, repo.IsFork)
+						if testCase.opts.Fork.Has() {
+							assert.Equal(t, testCase.opts.Fork.Value(), repo.IsFork)
 						}
 
-						switch testCase.opts.Mirror {
-						case util.OptionalBoolFalse:
-							assert.False(t, repo.IsMirror)
-						case util.OptionalBoolTrue:
-							assert.True(t, repo.IsMirror)
+						if testCase.opts.Mirror.Has() {
+							assert.Equal(t, testCase.opts.Mirror.Value(), repo.IsMirror)
 						}
 					}
 
 					if testCase.opts.OwnerID > 0 && !testCase.opts.AllPublic {
-						switch testCase.opts.Collaborate {
-						case util.OptionalBoolFalse:
-							assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID)
-						case util.OptionalBoolTrue:
-							assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID)
+						if testCase.opts.Collaborate.Has() {
+							if testCase.opts.Collaborate.Value() {
+								assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID)
+							} else {
+								assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID)
+							}
 						}
 					}
 				}
diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go
index ca9209d751..c13b698abf 100644
--- a/models/repo/repo_test.go
+++ b/models/repo/repo_test.go
@@ -12,17 +12,17 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/test"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
 )
 
 var (
 	countRepospts        = repo_model.CountRepositoryOptions{OwnerID: 10}
-	countReposptsPublic  = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolFalse}
-	countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolTrue}
+	countReposptsPublic  = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)}
+	countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)}
 )
 
 func TestGetRepositoryCount(t *testing.T) {
@@ -64,16 +64,17 @@ func TestRepoAPIURL(t *testing.T) {
 
 func TestWatchRepo(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	const repoID = 3
-	const userID = 2
 
-	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, true))
-	unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
-	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 
-	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, false))
-	unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
-	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
+	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true))
+	unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
+	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
+
+	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false))
+	unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
+	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 }
 
 func TestMetas(t *testing.T) {
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index 8a3ba1ee89..5a841f4d31 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -122,6 +122,7 @@ type PullRequestsConfig struct {
 	AllowRebase                   bool
 	AllowRebaseMerge              bool
 	AllowSquash                   bool
+	AllowFastForwardOnly          bool
 	AllowManualMerge              bool
 	AutodetectManualMerge         bool
 	AllowRebaseUpdate             bool
@@ -148,6 +149,7 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
 		mergeStyle == MergeStyleRebase && cfg.AllowRebase ||
 		mergeStyle == MergeStyleRebaseMerge && cfg.AllowRebaseMerge ||
 		mergeStyle == MergeStyleSquash && cfg.AllowSquash ||
+		mergeStyle == MergeStyleFastForwardOnly && cfg.AllowFastForwardOnly ||
 		mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge
 }
 
@@ -200,6 +202,53 @@ func (cfg *ActionsConfig) ToDB() ([]byte, error) {
 	return json.Marshal(cfg)
 }
 
+// ProjectsMode represents the projects enabled for a repository
+type ProjectsMode string
+
+const (
+	// ProjectsModeRepo allows only repo-level projects
+	ProjectsModeRepo ProjectsMode = "repo"
+	// ProjectsModeOwner allows only owner-level projects
+	ProjectsModeOwner ProjectsMode = "owner"
+	// ProjectsModeAll allows both kinds of projects
+	ProjectsModeAll ProjectsMode = "all"
+	// ProjectsModeNone doesn't allow projects
+	ProjectsModeNone ProjectsMode = "none"
+)
+
+// ProjectsConfig describes projects config
+type ProjectsConfig struct {
+	ProjectsMode ProjectsMode
+}
+
+// FromDB fills up a ProjectsConfig from serialized format.
+func (cfg *ProjectsConfig) FromDB(bs []byte) error {
+	return json.UnmarshalHandleDoubleEncode(bs, &cfg)
+}
+
+// ToDB exports a ProjectsConfig to a serialized format.
+func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
+	return json.Marshal(cfg)
+}
+
+func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
+	if cfg.ProjectsMode != "" {
+		return cfg.ProjectsMode
+	}
+
+	return ProjectsModeAll
+}
+
+func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
+	projectsMode := cfg.GetProjectsMode()
+
+	if m == ProjectsModeNone {
+		return true
+	}
+
+	return projectsMode == m || projectsMode == ProjectsModeAll
+}
+
 // BeforeSet is invoked from XORM before setting the value of a field of this object.
 func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
 	switch colName {
@@ -215,7 +264,9 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
 			r.Config = new(IssuesConfig)
 		case unit.TypeActions:
 			r.Config = new(ActionsConfig)
-		case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages:
+		case unit.TypeProjects:
+			r.Config = new(ProjectsConfig)
+		case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypePackages:
 			fallthrough
 		default:
 			r.Config = new(UnitConfig)
@@ -263,6 +314,11 @@ func (r *RepoUnit) ActionsConfig() *ActionsConfig {
 	return r.Config.(*ActionsConfig)
 }
 
+// ProjectsConfig returns config for unit.ProjectsConfig
+func (r *RepoUnit) ProjectsConfig() *ProjectsConfig {
+	return r.Config.(*ProjectsConfig)
+}
+
 func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
 	var tmpUnits []*RepoUnit
 	if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {
diff --git a/models/repo/star.go b/models/repo/star.go
index 60737149da..4c66855525 100644
--- a/models/repo/star.go
+++ b/models/repo/star.go
@@ -24,26 +24,30 @@ func init() {
 }
 
 // StarRepo or unstar repository.
-func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
+func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star bool) error {
 	ctx, committer, err := db.TxContext(ctx)
 	if err != nil {
 		return err
 	}
 	defer committer.Close()
-	staring := IsStaring(ctx, userID, repoID)
+	staring := IsStaring(ctx, doer.ID, repo.ID)
 
 	if star {
+		if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
+			return user_model.ErrBlockedUser
+		}
+
 		if staring {
 			return nil
 		}
 
-		if err := db.Insert(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
+		if err := db.Insert(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
 			return err
 		}
-		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil {
+		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repo.ID); err != nil {
 			return err
 		}
-		if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", userID); err != nil {
+		if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", doer.ID); err != nil {
 			return err
 		}
 	} else {
@@ -51,13 +55,13 @@ func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
 			return nil
 		}
 
-		if _, err := db.DeleteByBean(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
+		if _, err := db.DeleteByBean(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
 			return err
 		}
-		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoID); err != nil {
+		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repo.ID); err != nil {
 			return err
 		}
-		if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", userID); err != nil {
+		if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", doer.ID); err != nil {
 			return err
 		}
 	}
diff --git a/models/repo/star_test.go b/models/repo/star_test.go
index 62eac4e29a..aaac89d975 100644
--- a/models/repo/star_test.go
+++ b/models/repo/star_test.go
@@ -9,21 +9,24 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 
 	"github.com/stretchr/testify/assert"
 )
 
 func TestStarRepo(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	const userID = 2
-	const repoID = 1
-	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
-	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
-	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
-	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
-	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
-	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
-	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
+	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
+	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
+	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
 }
 
 func TestIsStaring(t *testing.T) {
@@ -54,17 +57,18 @@ func TestRepository_GetStargazers2(t *testing.T) {
 
 func TestClearRepoStars(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	const userID = 2
-	const repoID = 1
-	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
-	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
-	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
-	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
-	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
-	assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repoID))
-	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
 
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
+	unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+	assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
+	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+	assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repo.ID))
+	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
+
 	gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0})
 	assert.NoError(t, err)
 	assert.Len(t, gazers, 0)
diff --git a/models/repo/topic.go b/models/repo/topic.go
index 79b13e320d..430a60f603 100644
--- a/models/repo/topic.go
+++ b/models/repo/topic.go
@@ -178,7 +178,7 @@ type FindTopicOptions struct {
 	Keyword string
 }
 
-func (opts *FindTopicOptions) toConds() builder.Cond {
+func (opts *FindTopicOptions) ToConds() builder.Cond {
 	cond := builder.NewCond()
 	if opts.RepoID > 0 {
 		cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID})
@@ -191,29 +191,24 @@ func (opts *FindTopicOptions) toConds() builder.Cond {
 	return cond
 }
 
-// FindTopics retrieves the topics via FindTopicOptions
-func FindTopics(ctx context.Context, opts *FindTopicOptions) ([]*Topic, int64, error) {
-	sess := db.GetEngine(ctx).Select("topic.*").Where(opts.toConds())
+func (opts *FindTopicOptions) ToOrders() string {
 	orderBy := "topic.repo_count DESC"
 	if opts.RepoID > 0 {
-		sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
 		orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result
 	}
-	if opts.PageSize != 0 && opts.Page != 0 {
-		sess = db.SetSessionPagination(sess, opts)
-	}
-	topics := make([]*Topic, 0, 10)
-	total, err := sess.OrderBy(orderBy).FindAndCount(&topics)
-	return topics, total, err
+	return orderBy
 }
 
-// CountTopics counts the number of topics matching the FindTopicOptions
-func CountTopics(ctx context.Context, opts *FindTopicOptions) (int64, error) {
-	sess := db.GetEngine(ctx).Where(opts.toConds())
-	if opts.RepoID > 0 {
-		sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
+func (opts *FindTopicOptions) ToJoins() []db.JoinFunc {
+	if opts.RepoID <= 0 {
+		return nil
+	}
+	return []db.JoinFunc{
+		func(e db.Engine) error {
+			e.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
+			return nil
+		},
 	}
-	return sess.Count(new(Topic))
 }
 
 // GetRepoTopicByName retrieves topic from name for a repo if it exist
@@ -283,7 +278,7 @@ func DeleteTopic(ctx context.Context, repoID int64, topicName string) (*Topic, e
 
 // SaveTopics save topics to a repository
 func SaveTopics(ctx context.Context, repoID int64, topicNames ...string) error {
-	topics, _, err := FindTopics(ctx, &FindTopicOptions{
+	topics, err := db.Find[Topic](ctx, &FindTopicOptions{
 		RepoID: repoID,
 	})
 	if err != nil {
diff --git a/models/repo/topic_test.go b/models/repo/topic_test.go
index 2b609e6d66..1600896b6e 100644
--- a/models/repo/topic_test.go
+++ b/models/repo/topic_test.go
@@ -19,18 +19,18 @@ func TestAddTopic(t *testing.T) {
 
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	topics, _, err := repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{})
+	topics, err := db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{})
 	assert.NoError(t, err)
 	assert.Len(t, topics, totalNrOfTopics)
 
-	topics, total, err := repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
+	topics, total, err := db.FindAndCount[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
 		ListOptions: db.ListOptions{Page: 1, PageSize: 2},
 	})
 	assert.NoError(t, err)
 	assert.Len(t, topics, 2)
 	assert.EqualValues(t, 6, total)
 
-	topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
+	topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
 		RepoID: 1,
 	})
 	assert.NoError(t, err)
@@ -38,11 +38,11 @@ func TestAddTopic(t *testing.T) {
 
 	assert.NoError(t, repo_model.SaveTopics(db.DefaultContext, 2, "golang"))
 	repo2NrOfTopics := 1
-	topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{})
+	topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{})
 	assert.NoError(t, err)
 	assert.Len(t, topics, totalNrOfTopics)
 
-	topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
+	topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
 		RepoID: 2,
 	})
 	assert.NoError(t, err)
@@ -55,11 +55,11 @@ func TestAddTopic(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, 1, topic.RepoCount)
 
-	topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{})
+	topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{})
 	assert.NoError(t, err)
 	assert.Len(t, topics, totalNrOfTopics)
 
-	topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
+	topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
 		RepoID: 2,
 	})
 	assert.NoError(t, err)
diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go
index dd2ef62201..6862247657 100644
--- a/models/repo/user_repo.go
+++ b/models/repo/user_repo.go
@@ -8,6 +8,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
+	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
 	api "code.gitea.io/gitea/modules/structs"
@@ -15,47 +16,82 @@ import (
 	"xorm.io/builder"
 )
 
+type StarredReposOptions struct {
+	db.ListOptions
+	StarrerID      int64
+	RepoOwnerID    int64
+	IncludePrivate bool
+}
+
+func (opts *StarredReposOptions) ToConds() builder.Cond {
+	var cond builder.Cond = builder.Eq{
+		"star.uid": opts.StarrerID,
+	}
+	if opts.RepoOwnerID != 0 {
+		cond = cond.And(builder.Eq{
+			"repository.owner_id": opts.RepoOwnerID,
+		})
+	}
+	if !opts.IncludePrivate {
+		cond = cond.And(builder.Eq{
+			"repository.is_private": false,
+		})
+	}
+	return cond
+}
+
+func (opts *StarredReposOptions) ToJoins() []db.JoinFunc {
+	return []db.JoinFunc{
+		func(e db.Engine) error {
+			e.Join("INNER", "star", "`repository`.id=`star`.repo_id")
+			return nil
+		},
+	}
+}
+
 // GetStarredRepos returns the repos starred by a particular user
-func GetStarredRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, error) {
-	sess := db.GetEngine(ctx).
-		Where("star.uid=?", userID).
-		Join("LEFT", "star", "`repository`.id=`star`.repo_id")
-	if !private {
-		sess = sess.And("is_private=?", false)
+func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Repository, error) {
+	return db.Find[Repository](ctx, opts)
+}
+
+type WatchedReposOptions struct {
+	db.ListOptions
+	WatcherID      int64
+	RepoOwnerID    int64
+	IncludePrivate bool
+}
+
+func (opts *WatchedReposOptions) ToConds() builder.Cond {
+	var cond builder.Cond = builder.Eq{
+		"watch.user_id": opts.WatcherID,
 	}
-
-	if listOptions.Page != 0 {
-		sess = db.SetSessionPagination(sess, &listOptions)
-
-		repos := make([]*Repository, 0, listOptions.PageSize)
-		return repos, sess.Find(&repos)
+	if opts.RepoOwnerID != 0 {
+		cond = cond.And(builder.Eq{
+			"repository.owner_id": opts.RepoOwnerID,
+		})
 	}
+	if !opts.IncludePrivate {
+		cond = cond.And(builder.Eq{
+			"repository.is_private": false,
+		})
+	}
+	return cond.And(builder.Neq{
+		"watch.mode": WatchModeDont,
+	})
+}
 
-	repos := make([]*Repository, 0, 10)
-	return repos, sess.Find(&repos)
+func (opts *WatchedReposOptions) ToJoins() []db.JoinFunc {
+	return []db.JoinFunc{
+		func(e db.Engine) error {
+			e.Join("INNER", "watch", "`repository`.id=`watch`.repo_id")
+			return nil
+		},
+	}
 }
 
 // GetWatchedRepos returns the repos watched by a particular user
-func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, int64, error) {
-	sess := db.GetEngine(ctx).
-		Where("watch.user_id=?", userID).
-		And("`watch`.mode<>?", WatchModeDont).
-		Join("LEFT", "watch", "`repository`.id=`watch`.repo_id")
-	if !private {
-		sess = sess.And("is_private=?", false)
-	}
-
-	if listOptions.Page != 0 {
-		sess = db.SetSessionPagination(sess, &listOptions)
-
-		repos := make([]*Repository, 0, listOptions.PageSize)
-		total, err := sess.FindAndCount(&repos)
-		return repos, total, err
-	}
-
-	repos := make([]*Repository, 0, 10)
-	total, err := sess.FindAndCount(&repos)
-	return repos, total, err
+func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Repository, int64, error) {
+	return db.FindAndCount[Repository](ctx, opts)
 }
 
 // GetRepoAssignees returns all users that have write access and can be assigned to issues
@@ -78,7 +114,8 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
 	if err = e.Table("team_user").
 		Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
 		Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
-		Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode >= ?", repo.ID, perm.AccessModeWrite).
+		Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
+			repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
 		Distinct("`team_user`.uid").
 		Select("`team_user`.uid").
 		Find(&additionalUserIDs); err != nil {
diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go
index 7816b0262a..591dcea5b5 100644
--- a/models/repo/user_repo_test.go
+++ b/models/repo/user_repo_test.go
@@ -25,10 +25,8 @@ func TestRepoAssignees(t *testing.T) {
 	repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
 	users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
 	assert.NoError(t, err)
-	assert.Len(t, users, 3)
-	assert.Equal(t, users[0].ID, int64(15))
-	assert.Equal(t, users[1].ID, int64(18))
-	assert.Equal(t, users[2].ID, int64(16))
+	assert.Len(t, users, 4)
+	assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID})
 }
 
 func TestRepoGetReviewers(t *testing.T) {
diff --git a/models/repo/watch.go b/models/repo/watch.go
index 80da4030cb..a616544cae 100644
--- a/models/repo/watch.go
+++ b/models/repo/watch.go
@@ -104,29 +104,23 @@ func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error)
 	return err
 }
 
-// WatchRepoMode watch repository in specific mode.
-func WatchRepoMode(ctx context.Context, userID, repoID int64, mode WatchMode) (err error) {
-	var watch Watch
-	if watch, err = GetWatch(ctx, userID, repoID); err != nil {
-		return err
-	}
-	return watchRepoMode(ctx, watch, mode)
-}
-
 // WatchRepo watch or unwatch repository.
-func WatchRepo(ctx context.Context, userID, repoID int64, doWatch bool) (err error) {
-	var watch Watch
-	if watch, err = GetWatch(ctx, userID, repoID); err != nil {
+func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error {
+	watch, err := GetWatch(ctx, doer.ID, repo.ID)
+	if err != nil {
 		return err
 	}
 	if !doWatch && watch.Mode == WatchModeAuto {
-		err = watchRepoMode(ctx, watch, WatchModeDont)
+		return watchRepoMode(ctx, watch, WatchModeDont)
 	} else if !doWatch {
-		err = watchRepoMode(ctx, watch, WatchModeNone)
-	} else {
-		err = watchRepoMode(ctx, watch, WatchModeNormal)
+		return watchRepoMode(ctx, watch, WatchModeNone)
 	}
-	return err
+
+	if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
+		return user_model.ErrBlockedUser
+	}
+
+	return watchRepoMode(ctx, watch, WatchModeNormal)
 }
 
 // GetWatchers returns all watchers of given repository.
diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go
index 7aa899291c..a95a267961 100644
--- a/models/repo/watch_test.go
+++ b/models/repo/watch_test.go
@@ -9,6 +9,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/stretchr/testify/assert"
@@ -64,6 +65,8 @@ func TestWatchIfAuto(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	user12 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12})
+
 	watchers, err := repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
 	assert.NoError(t, err)
 	assert.Len(t, watchers, repo.NumWatches)
@@ -105,7 +108,7 @@ func TestWatchIfAuto(t *testing.T) {
 	assert.Len(t, watchers, prevCount+1)
 
 	// Should remove watch, inhibit from adding auto
-	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, 12, 1, false))
+	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user12, repo, false))
 	watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
 	assert.NoError(t, err)
 	assert.Len(t, watchers, prevCount)
@@ -116,24 +119,3 @@ func TestWatchIfAuto(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, watchers, prevCount)
 }
-
-func TestWatchRepoMode(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
-
-	assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeAuto))
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeAuto}, 1)
-
-	assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNormal))
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeNormal}, 1)
-
-	assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeDont))
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeDont}, 1)
-
-	assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNone))
-	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
-}
diff --git a/models/repo_transfer.go b/models/repo_transfer.go
index 676e2dbb63..747ec2f248 100644
--- a/models/repo_transfer.go
+++ b/models/repo_transfer.go
@@ -13,6 +13,8 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
+
+	"xorm.io/builder"
 )
 
 // RepoTransfer is used to manage repository transfers
@@ -94,21 +96,46 @@ func (r *RepoTransfer) CanUserAcceptTransfer(ctx context.Context, u *user_model.
 	return allowed
 }
 
+type PendingRepositoryTransferOptions struct {
+	RepoID      int64
+	SenderID    int64
+	RecipientID int64
+}
+
+func (opts *PendingRepositoryTransferOptions) ToConds() builder.Cond {
+	cond := builder.NewCond()
+	if opts.RepoID != 0 {
+		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
+	}
+	if opts.SenderID != 0 {
+		cond = cond.And(builder.Eq{"doer_id": opts.SenderID})
+	}
+	if opts.RecipientID != 0 {
+		cond = cond.And(builder.Eq{"recipient_id": opts.RecipientID})
+	}
+	return cond
+}
+
+func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryTransferOptions) ([]*RepoTransfer, error) {
+	transfers := make([]*RepoTransfer, 0, 10)
+	return transfers, db.GetEngine(ctx).
+		Where(opts.ToConds()).
+		Find(&transfers)
+}
+
 // GetPendingRepositoryTransfer fetches the most recent and ongoing transfer
 // process for the repository
 func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) (*RepoTransfer, error) {
-	transfer := new(RepoTransfer)
-
-	has, err := db.GetEngine(ctx).Where("repo_id = ? ", repo.ID).Get(transfer)
+	transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID})
 	if err != nil {
 		return nil, err
 	}
 
-	if !has {
+	if len(transfers) != 1 {
 		return nil, ErrNoPendingRepoTransfer{RepoID: repo.ID}
 	}
 
-	return transfer, nil
+	return transfers[0], nil
 }
 
 func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error {
diff --git a/models/secret/secret.go b/models/secret/secret.go
index 41e860d7f6..35bed500b9 100644
--- a/models/secret/secret.go
+++ b/models/secret/secret.go
@@ -9,7 +9,10 @@ import (
 	"fmt"
 	"strings"
 
+	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
+	actions_module "code.gitea.io/gitea/modules/actions"
+	"code.gitea.io/gitea/modules/log"
 	secret_module "code.gitea.io/gitea/modules/secret"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -112,3 +115,39 @@ func UpdateSecret(ctx context.Context, secretID int64, data string) error {
 	}
 	return err
 }
+
+func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[string]string, error) {
+	secrets := map[string]string{}
+
+	secrets["GITHUB_TOKEN"] = task.Token
+	secrets["GITEA_TOKEN"] = task.Token
+
+	if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget {
+		// ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated.
+		// for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch
+		// see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
+		return secrets, nil
+	}
+
+	ownerSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID})
+	if err != nil {
+		log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err)
+		return nil, err
+	}
+	repoSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{RepoID: task.Job.Run.RepoID})
+	if err != nil {
+		log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err)
+		return nil, err
+	}
+
+	for _, secret := range append(ownerSecrets, repoSecrets...) {
+		v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data)
+		if err != nil {
+			log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err)
+			return nil, err
+		}
+		secrets[secret.Name] = v
+	}
+
+	return secrets, nil
+}
diff --git a/models/shared/types/ownertype.go b/models/shared/types/ownertype.go
index e6fe4e4cfd..a1d46c986f 100644
--- a/models/shared/types/ownertype.go
+++ b/models/shared/types/ownertype.go
@@ -17,13 +17,13 @@ const (
 func (o OwnerType) LocaleString(locale translation.Locale) string {
 	switch o {
 	case OwnerTypeSystemGlobal:
-		return locale.Tr("concept_system_global")
+		return locale.TrString("concept_system_global")
 	case OwnerTypeIndividual:
-		return locale.Tr("concept_user_individual")
+		return locale.TrString("concept_user_individual")
 	case OwnerTypeRepository:
-		return locale.Tr("concept_code_repository")
+		return locale.TrString("concept_code_repository")
 	case OwnerTypeOrganization:
-		return locale.Tr("concept_user_organization")
+		return locale.TrString("concept_user_organization")
 	}
-	return locale.Tr("unknown")
+	return locale.TrString("unknown")
 }
diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go
index 4c668ad04b..cb90c12f2b 100644
--- a/models/unittest/testdb.go
+++ b/models/unittest/testdb.go
@@ -44,12 +44,12 @@ func fatalTestError(fmtStr string, args ...any) {
 }
 
 // InitSettings initializes config provider and load common settings for tests
-func InitSettings(extraConfigs ...string) {
+func InitSettings() {
 	if setting.CustomConf == "" {
 		setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
 		_ = os.Remove(setting.CustomConf)
 	}
-	setting.InitCfgProvider(setting.CustomConf, strings.Join(extraConfigs, "\n"))
+	setting.InitCfgProvider(setting.CustomConf)
 	setting.LoadCommonSettings()
 
 	if err := setting.PrepareAppDataPath(); err != nil {
diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go
index d47bceea1e..75898436fc 100644
--- a/models/unittest/unit_tests.go
+++ b/models/unittest/unit_tests.go
@@ -131,8 +131,8 @@ func AssertSuccessfulInsert(t assert.TestingT, beans ...any) {
 }
 
 // AssertCount assert the count of a bean
-func AssertCount(t assert.TestingT, bean, expected any) {
-	assert.EqualValues(t, expected, GetCount(t, bean))
+func AssertCount(t assert.TestingT, bean, expected any) bool {
+	return assert.EqualValues(t, expected, GetCount(t, bean))
 }
 
 // AssertInt64InRange assert value is in range [low, high]
@@ -150,7 +150,7 @@ func GetCountByCond(t assert.TestingT, tableName string, cond builder.Cond) int6
 }
 
 // AssertCountByCond test the count of database entries matching bean
-func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, expected int) {
-	assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
+func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, expected int) bool {
+	return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
 		"Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond)
 }
diff --git a/models/user/badge.go b/models/user/badge.go
index ee52b44cf5..3ff3530a36 100644
--- a/models/user/badge.go
+++ b/models/user/badge.go
@@ -5,13 +5,15 @@ package user
 
 import (
 	"context"
+	"fmt"
 
 	"code.gitea.io/gitea/models/db"
 )
 
 // Badge represents a user badge
 type Badge struct {
-	ID          int64 `xorm:"pk autoincr"`
+	ID          int64  `xorm:"pk autoincr"`
+	Slug        string `xorm:"UNIQUE"`
 	Description string
 	ImageURL    string
 }
@@ -39,3 +41,84 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
 	count, err := sess.FindAndCount(&badges)
 	return badges, count, err
 }
+
+// CreateBadge creates a new badge.
+func CreateBadge(ctx context.Context, badge *Badge) error {
+	_, err := db.GetEngine(ctx).Insert(badge)
+	return err
+}
+
+// GetBadge returns a badge
+func GetBadge(ctx context.Context, slug string) (*Badge, error) {
+	badge := new(Badge)
+	has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge)
+	if !has {
+		return nil, err
+	}
+	return badge, err
+}
+
+// UpdateBadge updates a badge based on its slug.
+func UpdateBadge(ctx context.Context, badge *Badge) error {
+	_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge)
+	return err
+}
+
+// DeleteBadge deletes a badge.
+func DeleteBadge(ctx context.Context, badge *Badge) error {
+	_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge)
+	return err
+}
+
+// AddUserBadge adds a badge to a user.
+func AddUserBadge(ctx context.Context, u *User, badge *Badge) error {
+	return AddUserBadges(ctx, u, []*Badge{badge})
+}
+
+// AddUserBadges adds badges to a user.
+func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		for _, badge := range badges {
+			// hydrate badge and check if it exists
+			has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge)
+			if err != nil {
+				return err
+			} else if !has {
+				return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug)
+			}
+			if err := db.Insert(ctx, &UserBadge{
+				BadgeID: badge.ID,
+				UserID:  u.ID,
+			}); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
+
+// RemoveUserBadge removes a badge from a user.
+func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
+	return RemoveUserBadges(ctx, u, []*Badge{badge})
+}
+
+// RemoveUserBadges removes badges from a user.
+func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		for _, badge := range badges {
+			if _, err := db.GetEngine(ctx).
+				Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
+				Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug).
+				Delete(&UserBadge{}); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
+
+// RemoveAllUserBadges removes all badges from a user.
+func RemoveAllUserBadges(ctx context.Context, u *User) error {
+	_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
+	return err
+}
diff --git a/models/user/block.go b/models/user/block.go
new file mode 100644
index 0000000000..5f2b65a199
--- /dev/null
+++ b/models/user/block.go
@@ -0,0 +1,123 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
+
+	"xorm.io/builder"
+)
+
+var (
+	ErrBlockOrganization = util.NewInvalidArgumentErrorf("cannot block an organization")
+	ErrCanNotBlock       = util.NewInvalidArgumentErrorf("cannot block the user")
+	ErrCanNotUnblock     = util.NewInvalidArgumentErrorf("cannot unblock the user")
+	ErrBlockedUser       = util.NewPermissionDeniedErrorf("user is blocked")
+)
+
+type Blocking struct {
+	ID          int64 `xorm:"pk autoincr"`
+	BlockerID   int64 `xorm:"UNIQUE(block)"`
+	Blocker     *User `xorm:"-"`
+	BlockeeID   int64 `xorm:"UNIQUE(block)"`
+	Blockee     *User `xorm:"-"`
+	Note        string
+	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+}
+
+func (*Blocking) TableName() string {
+	return "user_blocking"
+}
+
+func init() {
+	db.RegisterModel(new(Blocking))
+}
+
+func UpdateBlockingNote(ctx context.Context, id int64, note string) error {
+	_, err := db.GetEngine(ctx).ID(id).Cols("note").Update(&Blocking{Note: note})
+	return err
+}
+
+func IsUserBlockedBy(ctx context.Context, blockee *User, blockerIDs ...int64) bool {
+	if len(blockerIDs) == 0 {
+		return false
+	}
+
+	if blockee.IsAdmin {
+		return false
+	}
+
+	cond := builder.Eq{"user_blocking.blockee_id": blockee.ID}.
+		And(builder.In("user_blocking.blocker_id", blockerIDs))
+
+	has, _ := db.GetEngine(ctx).Where(cond).Exist(&Blocking{})
+	return has
+}
+
+type FindBlockingOptions struct {
+	db.ListOptions
+	BlockerID int64
+	BlockeeID int64
+}
+
+func (opts *FindBlockingOptions) ToConds() builder.Cond {
+	cond := builder.NewCond()
+	if opts.BlockerID != 0 {
+		cond = cond.And(builder.Eq{"user_blocking.blocker_id": opts.BlockerID})
+	}
+	if opts.BlockeeID != 0 {
+		cond = cond.And(builder.Eq{"user_blocking.blockee_id": opts.BlockeeID})
+	}
+	return cond
+}
+
+func FindBlockings(ctx context.Context, opts *FindBlockingOptions) ([]*Blocking, int64, error) {
+	return db.FindAndCount[Blocking](ctx, opts)
+}
+
+func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, error) {
+	blocks, _, err := FindBlockings(ctx, &FindBlockingOptions{
+		BlockerID: blockerID,
+		BlockeeID: blockeeID,
+	})
+	if err != nil {
+		return nil, err
+	}
+	if len(blocks) == 0 {
+		return nil, nil
+	}
+	return blocks[0], nil
+}
+
+type BlockingList []*Blocking
+
+func (blocks BlockingList) LoadAttributes(ctx context.Context) error {
+	ids := make(container.Set[int64], len(blocks)*2)
+	for _, b := range blocks {
+		ids.Add(b.BlockerID)
+		ids.Add(b.BlockeeID)
+	}
+
+	userList, err := GetUsersByIDs(ctx, ids.Values())
+	if err != nil {
+		return err
+	}
+
+	userMap := make(map[int64]*User, len(userList))
+	for _, u := range userList {
+		userMap[u.ID] = u
+	}
+
+	for _, b := range blocks {
+		b.Blocker = userMap[b.BlockerID]
+		b.Blockee = userMap[b.BlockeeID]
+	}
+
+	return nil
+}
diff --git a/models/user/email_address.go b/models/user/email_address.go
index 957e72fe89..08771efe99 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/validation"
@@ -21,9 +22,6 @@ import (
 	"xorm.io/builder"
 )
 
-// ErrEmailNotActivated e-mail address has not been activated error
-var ErrEmailNotActivated = util.NewInvalidArgumentErrorf("e-mail address has not been activated")
-
 // ErrEmailCharIsNotSupported e-mail address contains unsupported character
 type ErrEmailCharIsNotSupported struct {
 	Email string
@@ -156,37 +154,18 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
 
 var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
 
-// ValidateEmail check if email is a allowed address
+// ValidateEmail check if email is a valid & allowed address
 func ValidateEmail(email string) error {
-	if len(email) == 0 {
-		return ErrEmailInvalid{email}
+	if err := validateEmailBasic(email); err != nil {
+		return err
 	}
+	return validateEmailDomain(email)
+}
 
-	if !emailRegexp.MatchString(email) {
-		return ErrEmailCharIsNotSupported{email}
-	}
-
-	if email[0] == '-' {
-		return ErrEmailInvalid{email}
-	}
-
-	if _, err := mail.ParseAddress(email); err != nil {
-		return ErrEmailInvalid{email}
-	}
-
-	// if there is no allow list, then check email against block list
-	if len(setting.Service.EmailDomainAllowList) == 0 &&
-		validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) {
-		return ErrEmailInvalid{email}
-	}
-
-	// if there is an allow list, then check email against allow list
-	if len(setting.Service.EmailDomainAllowList) > 0 &&
-		!validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) {
-		return ErrEmailInvalid{email}
-	}
-
-	return nil
+// ValidateEmailForAdmin check if email is a valid address when admins manually add or edit users
+func ValidateEmailForAdmin(email string) error {
+	return validateEmailBasic(email)
+	// In this case we do not need to check the email domain
 }
 
 func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) {
@@ -277,14 +256,6 @@ func IsEmailUsed(ctx context.Context, email string) (bool, error) {
 	return db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(&EmailAddress{})
 }
 
-// DeleteInactiveEmailAddresses deletes inactive email addresses
-func DeleteInactiveEmailAddresses(ctx context.Context) error {
-	_, err := db.GetEngine(ctx).
-		Where("is_activated = ?", false).
-		Delete(new(EmailAddress))
-	return err
-}
-
 // ActivateEmail activates the email address to given user.
 func ActivateEmail(ctx context.Context, email *EmailAddress) error {
 	ctx, committer, err := db.TxContext(ctx)
@@ -313,29 +284,27 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e
 	return UpdateUserCols(ctx, user, "rands")
 }
 
-// MakeEmailPrimary sets primary email address of given user.
-func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
-	has, err := db.GetEngine(ctx).Get(email)
-	if err != nil {
+func MakeActiveEmailPrimary(ctx context.Context, emailID int64) error {
+	return makeEmailPrimaryInternal(ctx, emailID, true)
+}
+
+func MakeInactiveEmailPrimary(ctx context.Context, emailID int64) error {
+	return makeEmailPrimaryInternal(ctx, emailID, false)
+}
+
+func makeEmailPrimaryInternal(ctx context.Context, emailID int64, isActive bool) error {
+	email := &EmailAddress{}
+	if has, err := db.GetEngine(ctx).ID(emailID).Where(builder.Eq{"is_activated": isActive}).Get(email); err != nil {
 		return err
 	} else if !has {
-		return ErrEmailAddressNotExist{Email: email.Email}
-	}
-
-	if !email.IsActivated {
-		return ErrEmailNotActivated
+		return ErrEmailAddressNotExist{}
 	}
 
 	user := &User{}
-	has, err = db.GetEngine(ctx).ID(email.UID).Get(user)
-	if err != nil {
+	if has, err := db.GetEngine(ctx).ID(email.UID).Get(user); err != nil {
 		return err
 	} else if !has {
-		return ErrUserNotExist{
-			UID:   email.UID,
-			Name:  "",
-			KeyID: 0,
-		}
+		return ErrUserNotExist{UID: email.UID}
 	}
 
 	ctx, committer, err := db.TxContext(ctx)
@@ -367,6 +336,21 @@ func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
 	return committer.Commit()
 }
 
+// ChangeInactivePrimaryEmail replaces the inactive primary email of a given user
+func ChangeInactivePrimaryEmail(ctx context.Context, uid int64, oldEmailAddr, newEmailAddr string) error {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		_, err := db.GetEngine(ctx).Where(builder.Eq{"uid": uid, "lower_email": strings.ToLower(oldEmailAddr)}).Delete(&EmailAddress{})
+		if err != nil {
+			return err
+		}
+		newEmail, err := InsertEmailAddress(ctx, &EmailAddress{UID: uid, Email: newEmailAddr})
+		if err != nil {
+			return err
+		}
+		return MakeInactiveEmailPrimary(ctx, newEmail.ID)
+	})
+}
+
 // VerifyActiveEmailCode verifies active email code when active account
 func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
 	minutes := setting.Service.ActiveCodeLives
@@ -406,8 +390,8 @@ type SearchEmailOptions struct {
 	db.ListOptions
 	Keyword     string
 	SortType    SearchEmailOrderBy
-	IsPrimary   util.OptionalBool
-	IsActivated util.OptionalBool
+	IsPrimary   optional.Option[bool]
+	IsActivated optional.Option[bool]
 }
 
 // SearchEmailResult is an e-mail address found in the user or email_address table
@@ -434,21 +418,15 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail
 		))
 	}
 
-	switch {
-	case opts.IsPrimary.IsTrue():
-		cond = cond.And(builder.Eq{"email_address.is_primary": true})
-	case opts.IsPrimary.IsFalse():
-		cond = cond.And(builder.Eq{"email_address.is_primary": false})
+	if opts.IsPrimary.Has() {
+		cond = cond.And(builder.Eq{"email_address.is_primary": opts.IsPrimary.Value()})
 	}
 
-	switch {
-	case opts.IsActivated.IsTrue():
-		cond = cond.And(builder.Eq{"email_address.is_activated": true})
-	case opts.IsActivated.IsFalse():
-		cond = cond.And(builder.Eq{"email_address.is_activated": false})
+	if opts.IsActivated.Has() {
+		cond = cond.And(builder.Eq{"email_address.is_activated": opts.IsActivated.Value()})
 	}
 
-	count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.ID = email_address.uid").
+	count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.id = email_address.uid").
 		Where(cond).Count(new(EmailAddress))
 	if err != nil {
 		return nil, 0, fmt.Errorf("Count: %w", err)
@@ -464,7 +442,7 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail
 	emails := make([]*SearchEmailResult, 0, opts.PageSize)
 	err = db.GetEngine(ctx).Table("email_address").
 		Select("email_address.*, `user`.name, `user`.full_name").
-		Join("INNER", "`user`", "`user`.ID = email_address.uid").
+		Join("INNER", "`user`", "`user`.id = email_address.uid").
 		Where(cond).
 		OrderBy(orderby).
 		Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).
@@ -529,3 +507,41 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
 
 	return committer.Commit()
 }
+
+// validateEmailBasic checks whether the email complies with the rules
+func validateEmailBasic(email string) error {
+	if len(email) == 0 {
+		return ErrEmailInvalid{email}
+	}
+
+	if !emailRegexp.MatchString(email) {
+		return ErrEmailCharIsNotSupported{email}
+	}
+
+	if email[0] == '-' {
+		return ErrEmailInvalid{email}
+	}
+
+	if _, err := mail.ParseAddress(email); err != nil {
+		return ErrEmailInvalid{email}
+	}
+
+	return nil
+}
+
+// validateEmailDomain checks whether the email domain is allowed or blocked
+func validateEmailDomain(email string) error {
+	if !IsEmailDomainAllowed(email) {
+		return ErrEmailInvalid{email}
+	}
+
+	return nil
+}
+
+func IsEmailDomainAllowed(email string) bool {
+	if len(setting.Service.EmailDomainAllowList) == 0 {
+		return !validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email)
+	}
+
+	return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email)
+}
diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go
index 140443f82f..c2e010d95b 100644
--- a/models/user/email_address_test.go
+++ b/models/user/email_address_test.go
@@ -9,7 +9,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -45,31 +45,22 @@ func TestIsEmailUsed(t *testing.T) {
 func TestMakeEmailPrimary(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	email := &user_model.EmailAddress{
-		Email: "user567890@example.com",
-	}
-	err := user_model.MakeEmailPrimary(db.DefaultContext, email)
+	err := user_model.MakeActiveEmailPrimary(db.DefaultContext, 9999999)
 	assert.Error(t, err)
-	assert.EqualError(t, err, user_model.ErrEmailAddressNotExist{Email: email.Email}.Error())
+	assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{})
 
-	email = &user_model.EmailAddress{
-		Email: "user11@example.com",
-	}
-	err = user_model.MakeEmailPrimary(db.DefaultContext, email)
+	email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user11@example.com"})
+	err = user_model.MakeActiveEmailPrimary(db.DefaultContext, email.ID)
 	assert.Error(t, err)
-	assert.EqualError(t, err, user_model.ErrEmailNotActivated.Error())
+	assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{}) // inactive email is considered as not exist for "MakeActiveEmailPrimary"
 
-	email = &user_model.EmailAddress{
-		Email: "user9999999@example.com",
-	}
-	err = user_model.MakeEmailPrimary(db.DefaultContext, email)
+	email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user9999999@example.com"})
+	err = user_model.MakeActiveEmailPrimary(db.DefaultContext, email.ID)
 	assert.Error(t, err)
 	assert.True(t, user_model.IsErrUserNotExist(err))
 
-	email = &user_model.EmailAddress{
-		Email: "user101@example.com",
-	}
-	err = user_model.MakeEmailPrimary(db.DefaultContext, email)
+	email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user101@example.com"})
+	err = user_model.MakeActiveEmailPrimary(db.DefaultContext, email.ID)
 	assert.NoError(t, err)
 
 	user, _ := user_model.GetUserByID(db.DefaultContext, int64(10))
@@ -137,14 +128,14 @@ func TestListEmails(t *testing.T) {
 	assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.UID == 27 }))
 
 	// Must find only primary addresses (i.e. from the `user` table)
-	opts = &user_model.SearchEmailOptions{IsPrimary: util.OptionalBoolTrue}
+	opts = &user_model.SearchEmailOptions{IsPrimary: optional.Some(true)}
 	emails, _, err = user_model.SearchEmails(db.DefaultContext, opts)
 	assert.NoError(t, err)
 	assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.IsPrimary }))
 	assert.False(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsPrimary }))
 
 	// Must find only inactive addresses (i.e. not validated)
-	opts = &user_model.SearchEmailOptions{IsActivated: util.OptionalBoolFalse}
+	opts = &user_model.SearchEmailOptions{IsActivated: optional.Some(false)}
 	emails, _, err = user_model.SearchEmails(db.DefaultContext, opts)
 	assert.NoError(t, err)
 	assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsActivated }))
diff --git a/models/user/error.go b/models/user/error.go
index ef572c178a..cbf19998d1 100644
--- a/models/user/error.go
+++ b/models/user/error.go
@@ -31,9 +31,8 @@ func (err ErrUserAlreadyExist) Unwrap() error {
 
 // ErrUserNotExist represents a "UserNotExist" kind of error.
 type ErrUserNotExist struct {
-	UID   int64
-	Name  string
-	KeyID int64
+	UID  int64
+	Name string
 }
 
 // IsErrUserNotExist checks if an error is a ErrUserNotExist.
@@ -43,7 +42,7 @@ func IsErrUserNotExist(err error) bool {
 }
 
 func (err ErrUserNotExist) Error() string {
-	return fmt.Sprintf("user does not exist [uid: %d, name: %s, keyid: %d]", err.UID, err.Name, err.KeyID)
+	return fmt.Sprintf("user does not exist [uid: %d, name: %s]", err.UID, err.Name)
 }
 
 // Unwrap unwraps this error as a ErrNotExist error
diff --git a/models/user/follow.go b/models/user/follow.go
index f4dd2891ff..cf9672109a 100644
--- a/models/user/follow.go
+++ b/models/user/follow.go
@@ -29,26 +29,30 @@ func IsFollowing(ctx context.Context, userID, followID int64) bool {
 }
 
 // FollowUser marks someone be another's follower.
-func FollowUser(ctx context.Context, userID, followID int64) (err error) {
-	if userID == followID || IsFollowing(ctx, userID, followID) {
+func FollowUser(ctx context.Context, user, follow *User) (err error) {
+	if user.ID == follow.ID || IsFollowing(ctx, user.ID, follow.ID) {
 		return nil
 	}
 
+	if IsUserBlockedBy(ctx, user, follow.ID) || IsUserBlockedBy(ctx, follow, user.ID) {
+		return ErrBlockedUser
+	}
+
 	ctx, committer, err := db.TxContext(ctx)
 	if err != nil {
 		return err
 	}
 	defer committer.Close()
 
-	if err = db.Insert(ctx, &Follow{UserID: userID, FollowID: followID}); err != nil {
+	if err = db.Insert(ctx, &Follow{UserID: user.ID, FollowID: follow.ID}); err != nil {
 		return err
 	}
 
-	if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", followID); err != nil {
+	if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", follow.ID); err != nil {
 		return err
 	}
 
-	if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", userID); err != nil {
+	if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", user.ID); err != nil {
 		return err
 	}
 	return committer.Commit()
diff --git a/models/user/search.go b/models/user/search.go
index 0fa278c257..45b051187e 100644
--- a/models/user/search.go
+++ b/models/user/search.go
@@ -9,8 +9,9 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 
 	"xorm.io/builder"
 	"xorm.io/xorm"
@@ -30,11 +31,13 @@ type SearchUserOptions struct {
 	Actor         *User // The user doing the search
 	SearchByEmail bool  // Search by email as well as username/full name
 
-	IsActive           util.OptionalBool
-	IsAdmin            util.OptionalBool
-	IsRestricted       util.OptionalBool
-	IsTwoFactorEnabled util.OptionalBool
-	IsProhibitLogin    util.OptionalBool
+	SupportedSortOrders container.Set[string] // if not nil, only allow to use the sort orders in this set
+
+	IsActive           optional.Option[bool]
+	IsAdmin            optional.Option[bool]
+	IsRestricted       optional.Option[bool]
+	IsTwoFactorEnabled optional.Option[bool]
+	IsProhibitLogin    optional.Option[bool]
 	IncludeReserved    bool
 
 	ExtraParamStrings map[string]string
@@ -86,24 +89,24 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
 		cond = cond.And(builder.Eq{"login_name": opts.LoginName})
 	}
 
-	if !opts.IsActive.IsNone() {
-		cond = cond.And(builder.Eq{"is_active": opts.IsActive.IsTrue()})
+	if opts.IsActive.Has() {
+		cond = cond.And(builder.Eq{"is_active": opts.IsActive.Value()})
 	}
 
-	if !opts.IsAdmin.IsNone() {
-		cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()})
+	if opts.IsAdmin.Has() {
+		cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()})
 	}
 
-	if !opts.IsRestricted.IsNone() {
-		cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.IsTrue()})
+	if opts.IsRestricted.Has() {
+		cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.Value()})
 	}
 
-	if !opts.IsProhibitLogin.IsNone() {
-		cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.IsTrue()})
+	if opts.IsProhibitLogin.Has() {
+		cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.Value()})
 	}
 
 	e := db.GetEngine(ctx)
-	if opts.IsTwoFactorEnabled.IsNone() {
+	if !opts.IsTwoFactorEnabled.Has() {
 		return e.Where(cond)
 	}
 
@@ -111,7 +114,7 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
 	// While using LEFT JOIN, sometimes the performance might not be good, but it won't be a problem now, such SQL is seldom executed.
 	// There are some possible methods to refactor this SQL in future when we really need to optimize the performance (but not now):
 	// (1) add a column in user table (2) add a setting value in user_setting table (3) use search engines (bleve/elasticsearch)
-	if opts.IsTwoFactorEnabled.IsTrue() {
+	if opts.IsTwoFactorEnabled.Value() {
 		cond = cond.And(builder.Expr("two_factor.uid IS NOT NULL"))
 	} else {
 		cond = cond.And(builder.Expr("two_factor.uid IS NULL"))
@@ -128,7 +131,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _
 	defer sessCount.Close()
 	count, err := sessCount.Count(new(User))
 	if err != nil {
-		return nil, 0, fmt.Errorf("Count: %w", err)
+		return nil, 0, fmt.Errorf("count: %w", err)
 	}
 
 	if len(opts.OrderBy) == 0 {
diff --git a/models/user/user.go b/models/user/user.go
index e5245dfbb0..d459ec239e 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -25,6 +25,7 @@ import (
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -424,15 +425,15 @@ func (u *User) GetDisplayName() string {
 	return u.Name
 }
 
-// GetCompleteName returns the the full name and username in the form of
-// "Full Name (@username)" if full name is not empty, otherwise it returns
-// "@username".
+// GetCompleteName returns the full name and username in the form of
+// "Full Name (username)" if full name is not empty, otherwise it returns
+// "username".
 func (u *User) GetCompleteName() string {
 	trimmedFullName := strings.TrimSpace(u.FullName)
 	if len(trimmedFullName) > 0 {
-		return fmt.Sprintf("%s (@%s)", trimmedFullName, u.Name)
+		return fmt.Sprintf("%s (%s)", trimmedFullName, u.Name)
 	}
-	return fmt.Sprintf("@%s", u.Name)
+	return u.Name
 }
 
 func gitSafeName(name string) string {
@@ -573,18 +574,28 @@ func IsUsableUsername(name string) error {
 
 // CreateUserOverwriteOptions are an optional options who overwrite system defaults on user creation
 type CreateUserOverwriteOptions struct {
-	KeepEmailPrivate             util.OptionalBool
+	KeepEmailPrivate             optional.Option[bool]
 	Visibility                   *structs.VisibleType
-	AllowCreateOrganization      util.OptionalBool
+	AllowCreateOrganization      optional.Option[bool]
 	EmailNotificationsPreference *string
 	MaxRepoCreation              *int
 	Theme                        *string
-	IsRestricted                 util.OptionalBool
-	IsActive                     util.OptionalBool
+	IsRestricted                 optional.Option[bool]
+	IsActive                     optional.Option[bool]
 }
 
 // CreateUser creates record of a new user.
 func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
+	return createUser(ctx, u, false, overwriteDefault...)
+}
+
+// AdminCreateUser is used by admins to manually create users
+func AdminCreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
+	return createUser(ctx, u, true, overwriteDefault...)
+}
+
+// createUser creates record of a new user.
+func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
 	if err = IsUsableUsername(u.Name); err != nil {
 		return err
 	}
@@ -607,14 +618,14 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
 	// overwrite defaults if set
 	if len(overwriteDefault) != 0 && overwriteDefault[0] != nil {
 		overwrite := overwriteDefault[0]
-		if !overwrite.KeepEmailPrivate.IsNone() {
-			u.KeepEmailPrivate = overwrite.KeepEmailPrivate.IsTrue()
+		if overwrite.KeepEmailPrivate.Has() {
+			u.KeepEmailPrivate = overwrite.KeepEmailPrivate.Value()
 		}
 		if overwrite.Visibility != nil {
 			u.Visibility = *overwrite.Visibility
 		}
-		if !overwrite.AllowCreateOrganization.IsNone() {
-			u.AllowCreateOrganization = overwrite.AllowCreateOrganization.IsTrue()
+		if overwrite.AllowCreateOrganization.Has() {
+			u.AllowCreateOrganization = overwrite.AllowCreateOrganization.Value()
 		}
 		if overwrite.EmailNotificationsPreference != nil {
 			u.EmailNotificationsPreference = *overwrite.EmailNotificationsPreference
@@ -625,11 +636,11 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
 		if overwrite.Theme != nil {
 			u.Theme = *overwrite.Theme
 		}
-		if !overwrite.IsRestricted.IsNone() {
-			u.IsRestricted = overwrite.IsRestricted.IsTrue()
+		if overwrite.IsRestricted.Has() {
+			u.IsRestricted = overwrite.IsRestricted.Value()
 		}
-		if !overwrite.IsActive.IsNone() {
-			u.IsActive = overwrite.IsActive.IsTrue()
+		if overwrite.IsActive.Has() {
+			u.IsActive = overwrite.IsActive.Value()
 		}
 	}
 
@@ -638,8 +649,14 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
 		return err
 	}
 
-	if err := ValidateEmail(u.Email); err != nil {
-		return err
+	if createdByAdmin {
+		if err := ValidateEmailForAdmin(u.Email); err != nil {
+			return err
+		}
+	} else {
+		if err := ValidateEmail(u.Email); err != nil {
+			return err
+		}
 	}
 
 	ctx, committer, err := db.TxContext(ctx)
@@ -714,7 +731,7 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
 
 // IsLastAdminUser check whether user is the last admin
 func IsLastAdminUser(ctx context.Context, user *User) bool {
-	if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: util.OptionalBoolTrue}) <= 1 {
+	if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: optional.Some(true)}) <= 1 {
 		return true
 	}
 	return false
@@ -723,7 +740,7 @@ func IsLastAdminUser(ctx context.Context, user *User) bool {
 // CountUserFilter represent optional filters for CountUsers
 type CountUserFilter struct {
 	LastLoginSince *int64
-	IsAdmin        util.OptionalBool
+	IsAdmin        optional.Option[bool]
 }
 
 // CountUsers returns number of users.
@@ -741,8 +758,8 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
 			cond = cond.And(builder.Gte{"last_login_unix": *opts.LastLoginSince})
 		}
 
-		if !opts.IsAdmin.IsNone() {
-			cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()})
+		if opts.IsAdmin.Has() {
+			cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()})
 		}
 	}
 
@@ -835,7 +852,7 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) {
 	if err != nil {
 		return nil, err
 	} else if !has {
-		return nil, ErrUserNotExist{id, "", 0}
+		return nil, ErrUserNotExist{UID: id}
 	}
 	return u, nil
 }
@@ -885,14 +902,14 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
 // GetUserByNameCtx returns user by given name.
 func GetUserByName(ctx context.Context, name string) (*User, error) {
 	if len(name) == 0 {
-		return nil, ErrUserNotExist{0, name, 0}
+		return nil, ErrUserNotExist{Name: name}
 	}
 	u := &User{LowerName: strings.ToLower(name), Type: UserTypeIndividual}
 	has, err := db.GetEngine(ctx).Get(u)
 	if err != nil {
 		return nil, err
 	} else if !has {
-		return nil, ErrUserNotExist{0, name, 0}
+		return nil, ErrUserNotExist{Name: name}
 	}
 	return u, nil
 }
@@ -1033,7 +1050,7 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) []
 // GetUserByEmail returns the user object by given e-mail if exists.
 func GetUserByEmail(ctx context.Context, email string) (*User, error) {
 	if len(email) == 0 {
-		return nil, ErrUserNotExist{0, email, 0}
+		return nil, ErrUserNotExist{Name: email}
 	}
 
 	email = strings.ToLower(email)
@@ -1060,7 +1077,7 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
 		}
 	}
 
-	return nil, ErrUserNotExist{0, email, 0}
+	return nil, ErrUserNotExist{Name: email}
 }
 
 // GetUser checks if a user already exists
@@ -1071,7 +1088,7 @@ func GetUser(ctx context.Context, user *User) (bool, error) {
 // GetUserByOpenID returns the user object by given OpenID if exists.
 func GetUserByOpenID(ctx context.Context, uri string) (*User, error) {
 	if len(uri) == 0 {
-		return nil, ErrUserNotExist{0, uri, 0}
+		return nil, ErrUserNotExist{Name: uri}
 	}
 
 	uri, err := openid.Normalize(uri)
@@ -1091,7 +1108,7 @@ func GetUserByOpenID(ctx context.Context, uri string) (*User, error) {
 		return GetUserByID(ctx, oid.UID)
 	}
 
-	return nil, ErrUserNotExist{0, uri, 0}
+	return nil, ErrUserNotExist{Name: uri}
 }
 
 // GetAdminUser returns the first administrator
@@ -1166,7 +1183,7 @@ func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool {
 			return false
 		}
 
-		// If they follow - they see each over
+		// If they follow - they see each other
 		follower := IsFollowing(ctx, u.ID, viewer.ID)
 		if follower {
 			return true
@@ -1215,3 +1232,21 @@ func GetOrderByName() string {
 	}
 	return "name"
 }
+
+// IsFeatureDisabledWithLoginType checks if a user feature is disabled, taking into account the login type of the
+// user if applicable
+func IsFeatureDisabledWithLoginType(user *User, feature string) bool {
+	// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType
+	return (user != nil && user.LoginType > auth.Plain && setting.Admin.ExternalUserDisableFeatures.Contains(feature)) ||
+		setting.Admin.UserDisabledFeatures.Contains(feature)
+}
+
+// DisabledFeaturesWithLoginType returns the set of user features disabled, taking into account the login type
+// of the user if applicable
+func DisabledFeaturesWithLoginType(user *User) *container.Set[string] {
+	// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType
+	if user != nil && user.LoginType > auth.Plain {
+		return &setting.Admin.ExternalUserDisableFeatures
+	}
+	return &setting.Admin.UserDisabledFeatures
+}
diff --git a/models/user/user_test.go b/models/user/user_test.go
index f3e5a95b1e..a4550fa655 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -16,10 +16,11 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password/hash"
+	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -89,7 +90,7 @@ func TestSearchUsers(t *testing.T) {
 		[]int64{19, 25})
 
 	testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}},
-		[]int64{26})
+		[]int64{26, 41})
 
 	testOrgSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 5, PageSize: 2}},
 		[]int64{})
@@ -101,31 +102,31 @@ func TestSearchUsers(t *testing.T) {
 	}
 
 	testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}},
-		[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37})
+		[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
 
-	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse},
+	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)},
 		[]int64{9})
 
-	testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
-		[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37})
+	testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
+		[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
 
-	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
 		[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
 
 	// order by name asc default
-	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
 		[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
 
-	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)},
 		[]int64{1})
 
-	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)},
 		[]int64{29})
 
-	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)},
 		[]int64{37})
 
-	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: util.OptionalBoolTrue},
+	testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)},
 		[]int64{24})
 }
 
@@ -399,14 +400,19 @@ func TestGetUserByOpenID(t *testing.T) {
 func TestFollowUser(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	testSuccess := func(followerID, followedID int64) {
-		assert.NoError(t, user_model.FollowUser(db.DefaultContext, followerID, followedID))
-		unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
+	testSuccess := func(follower, followed *user_model.User) {
+		assert.NoError(t, user_model.FollowUser(db.DefaultContext, follower, followed))
+		unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: follower.ID, FollowID: followed.ID})
 	}
-	testSuccess(4, 2)
-	testSuccess(5, 2)
 
-	assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2))
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+	user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+
+	testSuccess(user4, user2)
+	testSuccess(user5, user2)
+
+	assert.NoError(t, user_model.FollowUser(db.DefaultContext, user2, user2))
 
 	unittest.CheckConsistencyFor(t, &user_model.User{})
 }
@@ -521,3 +527,37 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
 		}
 	}
 }
+
+func TestDisabledUserFeatures(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	testValues := container.SetOf(setting.UserFeatureDeletion,
+		setting.UserFeatureManageSSHKeys,
+		setting.UserFeatureManageGPGKeys)
+
+	oldSetting := setting.Admin.ExternalUserDisableFeatures
+	defer func() {
+		setting.Admin.ExternalUserDisableFeatures = oldSetting
+	}()
+	setting.Admin.ExternalUserDisableFeatures = testValues
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+	assert.Len(t, setting.Admin.UserDisabledFeatures.Values(), 0)
+
+	// no features should be disabled with a plain login type
+	assert.LessOrEqual(t, user.LoginType, auth.Plain)
+	assert.Len(t, user_model.DisabledFeaturesWithLoginType(user).Values(), 0)
+	for _, f := range testValues.Values() {
+		assert.False(t, user_model.IsFeatureDisabledWithLoginType(user, f))
+	}
+
+	// check disabled features with external login type
+	user.LoginType = auth.OAuth2
+
+	// all features should be disabled
+	assert.NotEmpty(t, user_model.DisabledFeaturesWithLoginType(user).Values())
+	for _, f := range testValues.Values() {
+		assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f))
+	}
+}
diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go
index 2fb655ebca..ff3fdbadb2 100644
--- a/models/webhook/hooktask.go
+++ b/models/webhook/hooktask.go
@@ -5,13 +5,13 @@ package webhook
 
 import (
 	"context"
+	"errors"
 	"time"
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -31,6 +31,7 @@ type HookRequest struct {
 	URL        string            `json:"url"`
 	HTTPMethod string            `json:"http_method"`
 	Headers    map[string]string `json:"headers"`
+	Body       string            `json:"body"`
 }
 
 // HookResponse represents hook task response information.
@@ -45,11 +46,15 @@ type HookTask struct {
 	ID             int64  `xorm:"pk autoincr"`
 	HookID         int64  `xorm:"index"`
 	UUID           string `xorm:"unique"`
-	api.Payloader  `xorm:"-"`
 	PayloadContent string `xorm:"LONGTEXT"`
-	EventType      webhook_module.HookEventType
-	IsDelivered    bool
-	Delivered      timeutil.TimeStampNano
+	// PayloadVersion number to allow for smooth version upgrades:
+	//  - PayloadVersion 1: PayloadContent contains the JSON as sent to the URL
+	//  - PayloadVersion 2: PayloadContent contains the original event
+	PayloadVersion int `xorm:"DEFAULT 1"`
+
+	EventType   webhook_module.HookEventType
+	IsDelivered bool
+	Delivered   timeutil.TimeStampNano
 
 	// History info.
 	IsSucceed       bool
@@ -115,16 +120,12 @@ func HookTasks(ctx context.Context, hookID int64, page int) ([]*HookTask, error)
 // it handles conversion from Payload to PayloadContent.
 func CreateHookTask(ctx context.Context, t *HookTask) (*HookTask, error) {
 	t.UUID = gouuid.New().String()
-	if t.Payloader != nil {
-		data, err := t.Payloader.JSONPayload()
-		if err != nil {
-			return nil, err
-		}
-		t.PayloadContent = string(data)
-	}
 	if t.Delivered == 0 {
 		t.Delivered = timeutil.TimeStampNanoNow()
 	}
+	if t.PayloadVersion == 0 {
+		return nil, errors.New("missing HookTask.PayloadVersion")
+	}
 	return t, db.Insert(ctx, t)
 }
 
@@ -165,6 +166,7 @@ func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask,
 		HookID:         task.HookID,
 		PayloadContent: task.PayloadContent,
 		EventType:      task.EventType,
+		PayloadVersion: task.PayloadVersion,
 	})
 }
 
diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go
index 4a84a3d411..894357e36a 100644
--- a/models/webhook/webhook.go
+++ b/models/webhook/webhook.go
@@ -12,6 +12,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/secret"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -433,7 +434,7 @@ type ListWebhookOptions struct {
 	db.ListOptions
 	RepoID   int64
 	OwnerID  int64
-	IsActive util.OptionalBool
+	IsActive optional.Option[bool]
 }
 
 func (opts ListWebhookOptions) ToConds() builder.Cond {
@@ -444,8 +445,8 @@ func (opts ListWebhookOptions) ToConds() builder.Cond {
 	if opts.OwnerID != 0 {
 		cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID})
 	}
-	if !opts.IsActive.IsNone() {
-		cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()})
+	if opts.IsActive.Has() {
+		cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.Value()})
 	}
 	return cond
 }
diff --git a/models/webhook/webhook_system.go b/models/webhook/webhook_system.go
index 2e89f9547b..a2a9ee321a 100644
--- a/models/webhook/webhook_system.go
+++ b/models/webhook/webhook_system.go
@@ -8,7 +8,7 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 )
 
 // GetDefaultWebhooks returns all admin-default webhooks.
@@ -34,15 +34,15 @@ func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error)
 }
 
 // GetSystemWebhooks returns all admin system webhooks.
-func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webhook, error) {
+func GetSystemWebhooks(ctx context.Context, isActive optional.Option[bool]) ([]*Webhook, error) {
 	webhooks := make([]*Webhook, 0, 5)
-	if isActive.IsNone() {
+	if !isActive.Has() {
 		return webhooks, db.GetEngine(ctx).
 			Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true).
 			Find(&webhooks)
 	}
 	return webhooks, db.GetEngine(ctx).
-		Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()).
+		Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.Value()).
 		Find(&webhooks)
 }
 
diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go
index 694fd7a873..f4403776ce 100644
--- a/models/webhook/webhook_test.go
+++ b/models/webhook/webhook_test.go
@@ -11,9 +11,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/json"
-	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
 	"github.com/stretchr/testify/assert"
@@ -35,8 +34,10 @@ func TestWebhook_History(t *testing.T) {
 	webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1})
 	tasks, err := webhook.History(db.DefaultContext, 0)
 	assert.NoError(t, err)
-	if assert.Len(t, tasks, 1) {
-		assert.Equal(t, int64(1), tasks[0].ID)
+	if assert.Len(t, tasks, 3) {
+		assert.Equal(t, int64(3), tasks[0].ID)
+		assert.Equal(t, int64(2), tasks[1].ID)
+		assert.Equal(t, int64(1), tasks[2].ID)
 	}
 
 	webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2})
@@ -123,7 +124,7 @@ func TestGetWebhookByOwnerID(t *testing.T) {
 
 func TestGetActiveWebhooksByRepoID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: util.OptionalBoolTrue})
+	hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)})
 	assert.NoError(t, err)
 	if assert.Len(t, hooks, 1) {
 		assert.Equal(t, int64(1), hooks[0].ID)
@@ -143,7 +144,7 @@ func TestGetWebhooksByRepoID(t *testing.T) {
 
 func TestGetActiveWebhooksByOwnerID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: util.OptionalBoolTrue})
+	hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)})
 	assert.NoError(t, err)
 	if assert.Len(t, hooks, 1) {
 		assert.Equal(t, int64(3), hooks[0].ID)
@@ -197,8 +198,10 @@ func TestHookTasks(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTasks, err := HookTasks(db.DefaultContext, 1, 1)
 	assert.NoError(t, err)
-	if assert.Len(t, hookTasks, 1) {
-		assert.Equal(t, int64(1), hookTasks[0].ID)
+	if assert.Len(t, hookTasks, 3) {
+		assert.Equal(t, int64(3), hookTasks[0].ID)
+		assert.Equal(t, int64(2), hookTasks[1].ID)
+		assert.Equal(t, int64(1), hookTasks[2].ID)
 	}
 
 	hookTasks, err = HookTasks(db.DefaultContext, unittest.NonexistentID, 1)
@@ -209,8 +212,8 @@ func TestHookTasks(t *testing.T) {
 func TestCreateHookTask(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:    3,
-		Payloader: &api.PushPayload{},
+		HookID:         3,
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -232,10 +235,10 @@ func TestUpdateHookTask(t *testing.T) {
 func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:      3,
-		Payloader:   &api.PushPayload{},
-		IsDelivered: true,
-		Delivered:   timeutil.TimeStampNanoNow(),
+		HookID:         3,
+		IsDelivered:    true,
+		Delivered:      timeutil.TimeStampNanoNow(),
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -249,9 +252,9 @@ func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
 func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:      4,
-		Payloader:   &api.PushPayload{},
-		IsDelivered: false,
+		HookID:         4,
+		IsDelivered:    false,
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -265,10 +268,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
 func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:      4,
-		Payloader:   &api.PushPayload{},
-		IsDelivered: true,
-		Delivered:   timeutil.TimeStampNanoNow(),
+		HookID:         4,
+		IsDelivered:    true,
+		Delivered:      timeutil.TimeStampNanoNow(),
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -282,10 +285,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
 func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:      3,
-		Payloader:   &api.PushPayload{},
-		IsDelivered: true,
-		Delivered:   timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()),
+		HookID:         3,
+		IsDelivered:    true,
+		Delivered:      timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()),
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -299,9 +302,9 @@ func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
 func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:      4,
-		Payloader:   &api.PushPayload{},
-		IsDelivered: false,
+		HookID:         4,
+		IsDelivered:    false,
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
@@ -315,10 +318,10 @@ func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
 func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	hookTask := &HookTask{
-		HookID:      4,
-		Payloader:   &api.PushPayload{},
-		IsDelivered: true,
-		Delivered:   timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()),
+		HookID:         4,
+		IsDelivered:    true,
+		Delivered:      timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()),
+		PayloadVersion: 2,
 	}
 	unittest.AssertNotExistsBean(t, hookTask)
 	_, err := CreateHookTask(db.DefaultContext, hookTask)
diff --git a/modules/actions/github.go b/modules/actions/github.go
index fafea4e11a..68116ec83a 100644
--- a/modules/actions/github.go
+++ b/modules/actions/github.go
@@ -25,6 +25,45 @@ const (
 	GithubEventSchedule                 = "schedule"
 )
 
+// IsDefaultBranchWorkflow returns true if the event only triggers workflows on the default branch
+func IsDefaultBranchWorkflow(triggedEvent webhook_module.HookEventType) bool {
+	switch triggedEvent {
+	case webhook_module.HookEventDelete:
+		// GitHub "delete" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#delete
+		return true
+	case webhook_module.HookEventFork:
+		// GitHub "fork" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#fork
+		return true
+	case webhook_module.HookEventIssueComment:
+		// GitHub "issue_comment" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment
+		return true
+	case webhook_module.HookEventPullRequestComment:
+		// GitHub "pull_request_comment" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
+		return true
+	case webhook_module.HookEventWiki:
+		// GitHub "gollum" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum
+		return true
+	case webhook_module.HookEventSchedule:
+		// GitHub "schedule" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
+		return true
+	case webhook_module.HookEventIssues,
+		webhook_module.HookEventIssueAssign,
+		webhook_module.HookEventIssueLabel,
+		webhook_module.HookEventIssueMilestone:
+		// Github "issues" event
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues
+		return true
+	}
+
+	return false
+}
+
 // canGithubEventMatch check if the input Github event can match any Gitea event.
 func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEventType) bool {
 	switch eventName {
@@ -52,7 +91,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
 		case webhook_module.HookEventPullRequest,
 			webhook_module.HookEventPullRequestSync,
 			webhook_module.HookEventPullRequestAssign,
-			webhook_module.HookEventPullRequestLabel:
+			webhook_module.HookEventPullRequestLabel,
+			webhook_module.HookEventPullRequestReviewRequest,
+			webhook_module.HookEventPullRequestMilestone:
 			return true
 
 		default:
@@ -73,6 +114,11 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
 	case GithubEventSchedule:
 		return triggedEvent == webhook_module.HookEventSchedule
 
+	case GithubEventIssueComment:
+		// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
+		return triggedEvent == webhook_module.HookEventIssueComment ||
+			triggedEvent == webhook_module.HookEventPullRequestComment
+
 	default:
 		return eventName == string(triggedEvent)
 	}
diff --git a/modules/actions/github_test.go b/modules/actions/github_test.go
index 4bf55ae03f..6652ff6eac 100644
--- a/modules/actions/github_test.go
+++ b/modules/actions/github_test.go
@@ -103,6 +103,12 @@ func TestCanGithubEventMatch(t *testing.T) {
 			webhook_module.HookEventCreate,
 			true,
 		},
+		{
+			"create pull request comment",
+			GithubEventIssueComment,
+			webhook_module.HookEventPullRequestComment,
+			true,
+		},
 	}
 
 	for _, tc := range testCases {
diff --git a/modules/actions/log.go b/modules/actions/log.go
index cdf18646aa..c38082b5dc 100644
--- a/modules/actions/log.go
+++ b/modules/actions/log.go
@@ -100,7 +100,7 @@ func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limi
 	}
 
 	if err := scanner.Err(); err != nil {
-		return nil, fmt.Errorf("scan: %w", err)
+		return nil, fmt.Errorf("ReadLogs scan: %w", err)
 	}
 
 	return rows, nil
diff --git a/modules/actions/task_state.go b/modules/actions/task_state.go
index cbbc0b357d..31a74be3fd 100644
--- a/modules/actions/task_state.go
+++ b/modules/actions/task_state.go
@@ -35,9 +35,18 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
 	} else if task.Status.IsDone() {
 		preStep.Stopped = task.Stopped
 		preStep.Status = actions_model.StatusFailure
+		if task.Status.IsSkipped() {
+			preStep.Status = actions_model.StatusSkipped
+		}
 	}
 	logIndex += preStep.LogLength
 
+	// lastHasRunStep is the last step that has run.
+	// For example,
+	// 1. preStep(Success) -> step1(Success) -> step2(Running) -> step3(Waiting) -> postStep(Waiting): lastHasRunStep is step1.
+	// 2. preStep(Success) -> step1(Success) -> step2(Success) -> step3(Success) -> postStep(Success): lastHasRunStep is step3.
+	// 3. preStep(Success) -> step1(Success) -> step2(Failure) -> step3 -> postStep(Waiting): lastHasRunStep is step2.
+	// So its Stopped is the Started of postStep when there are no more steps to run.
 	var lastHasRunStep *actions_model.ActionTaskStep
 	for _, step := range task.Steps {
 		if step.Status.HasRun() {
@@ -53,11 +62,15 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
 		Name:   postStepName,
 		Status: actions_model.StatusWaiting,
 	}
-	if task.Status.IsDone() {
+	// If the lastHasRunStep is the last step, or it has failed, postStep has started.
+	if lastHasRunStep.Status.IsFailure() || lastHasRunStep == task.Steps[len(task.Steps)-1] {
 		postStep.LogIndex = logIndex
 		postStep.LogLength = task.LogLength - postStep.LogIndex
-		postStep.Status = task.Status
 		postStep.Started = lastHasRunStep.Stopped
+		postStep.Status = actions_model.StatusRunning
+	}
+	if task.Status.IsDone() {
+		postStep.Status = task.Status
 		postStep.Stopped = task.Stopped
 	}
 	ret := make([]*actions_model.ActionTaskStep, 0, len(task.Steps)+2)
diff --git a/modules/actions/task_state_test.go b/modules/actions/task_state_test.go
index 3a599fbcbd..28213d781b 100644
--- a/modules/actions/task_state_test.go
+++ b/modules/actions/task_state_test.go
@@ -103,6 +103,40 @@ func TestFullSteps(t *testing.T) {
 				{Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 100, LogLength: 0, Started: 10100, Stopped: 10100},
 			},
 		},
+		{
+			name: "all steps finished but task is running",
+			task: &actions_model.ActionTask{
+				Steps: []*actions_model.ActionTaskStep{
+					{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090},
+				},
+				Status:    actions_model.StatusRunning,
+				Started:   10000,
+				Stopped:   0,
+				LogLength: 100,
+			},
+			want: []*actions_model.ActionTaskStep{
+				{Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 10, Started: 10000, Stopped: 10010},
+				{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090},
+				{Name: postStepName, Status: actions_model.StatusRunning, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 0},
+			},
+		},
+		{
+			name: "skipped task",
+			task: &actions_model.ActionTask{
+				Steps: []*actions_model.ActionTaskStep{
+					{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
+				},
+				Status:    actions_model.StatusSkipped,
+				Started:   0,
+				Stopped:   0,
+				LogLength: 0,
+			},
+			want: []*actions_model.ActionTaskStep{
+				{Name: preStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
+				{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
+				{Name: postStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go
index a883f4181b..595fd8bbb0 100644
--- a/modules/actions/workflows.go
+++ b/modules/actions/workflows.go
@@ -221,7 +221,9 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
 		webhook_module.HookEventPullRequest,
 		webhook_module.HookEventPullRequestSync,
 		webhook_module.HookEventPullRequestAssign,
-		webhook_module.HookEventPullRequestLabel:
+		webhook_module.HookEventPullRequestLabel,
+		webhook_module.HookEventPullRequestReviewRequest,
+		webhook_module.HookEventPullRequestMilestone:
 		return matchPullRequestEvent(gitRepo, commit, payload.(*api.PullRequestPayload), evt)
 
 	case // pull_request_review
@@ -397,13 +399,13 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
 	} else {
 		// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
 		// Actions with the same name:
-		// opened, edited, closed, reopened, assigned, unassigned
+		// opened, edited, closed, reopened, assigned, unassigned, review_requested, review_request_removed, milestoned, demilestoned
 		// Actions need to be converted:
 		// synchronized -> synchronize
 		// label_updated -> labeled
 		// label_cleared -> unlabeled
 		// Unsupported activity types:
-		// converted_to_draft, ready_for_review, locked, unlocked, review_requested, review_request_removed, auto_merge_enabled, auto_merge_disabled
+		// converted_to_draft, ready_for_review, locked, unlocked, auto_merge_enabled, auto_merge_disabled, enqueued, dequeued
 
 		action := prPayload.Action
 		switch action {
@@ -439,6 +441,9 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
 	// all acts conditions should be satisfied
 	for cond, vals := range acts {
 		switch cond {
+		case "types":
+			// types have been checked
+			continue
 		case "branches":
 			refName := git.RefName(prPayload.PullRequest.Base.Ref)
 			patterns, err := workflowpattern.CompilePatterns(vals...)
diff --git a/modules/auth/password/hash/pbkdf2.go b/modules/auth/password/hash/pbkdf2.go
index 9ff6d162fc..27382fedb8 100644
--- a/modules/auth/password/hash/pbkdf2.go
+++ b/modules/auth/password/hash/pbkdf2.go
@@ -4,12 +4,12 @@
 package hash
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"strings"
 
 	"code.gitea.io/gitea/modules/log"
 
-	"github.com/minio/sha256-simd"
 	"golang.org/x/crypto/pbkdf2"
 )
 
diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go
index 2c7205b708..27074358a9 100644
--- a/modules/auth/password/password.go
+++ b/modules/auth/password/password.go
@@ -8,6 +8,7 @@ import (
 	"context"
 	"crypto/rand"
 	"errors"
+	"html/template"
 	"math/big"
 	"strings"
 	"sync"
@@ -121,15 +122,15 @@ func Generate(n int) (string, error) {
 }
 
 // BuildComplexityError builds the error message when password complexity checks fail
-func BuildComplexityError(locale translation.Locale) string {
+func BuildComplexityError(locale translation.Locale) template.HTML {
 	var buffer bytes.Buffer
-	buffer.WriteString(locale.Tr("form.password_complexity"))
+	buffer.WriteString(locale.TrString("form.password_complexity"))
 	buffer.WriteString("<ul>")
 	for _, c := range requiredList {
 		buffer.WriteString("<li>")
-		buffer.WriteString(locale.Tr(c.TrNameOne))
+		buffer.WriteString(locale.TrString(c.TrNameOne))
 		buffer.WriteString("</li>")
 	}
 	buffer.WriteString("</ul>")
-	return buffer.String()
+	return template.HTML(buffer.String())
 }
diff --git a/modules/avatar/hash.go b/modules/avatar/hash.go
index 4fc28a7739..50db9c1943 100644
--- a/modules/avatar/hash.go
+++ b/modules/avatar/hash.go
@@ -4,10 +4,9 @@
 package avatar
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"strconv"
-
-	"github.com/minio/sha256-simd"
 )
 
 // HashAvatar will generate a unique string, which ensures that when there's a
diff --git a/modules/avatar/identicon/identicon.go b/modules/avatar/identicon/identicon.go
index 9b7a2faf05..63926d5f19 100644
--- a/modules/avatar/identicon/identicon.go
+++ b/modules/avatar/identicon/identicon.go
@@ -7,11 +7,10 @@
 package identicon
 
 import (
+	"crypto/sha256"
 	"fmt"
 	"image"
 	"image/color"
-
-	"github.com/minio/sha256-simd"
 )
 
 const minImageSize = 16
diff --git a/modules/badge/badge.go b/modules/badge/badge.go
new file mode 100644
index 0000000000..b30d0b4729
--- /dev/null
+++ b/modules/badge/badge.go
@@ -0,0 +1,104 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package badge
+
+import (
+	actions_model "code.gitea.io/gitea/models/actions"
+)
+
+// The Badge layout: |offset|label|message|
+// We use 10x scale to calculate more precisely
+// Then scale down to normal size in tmpl file
+
+type Label struct {
+	text  string
+	width int
+}
+
+func (l Label) Text() string {
+	return l.text
+}
+
+func (l Label) Width() int {
+	return l.width
+}
+
+func (l Label) TextLength() int {
+	return int(float64(l.width-defaultOffset) * 9.5)
+}
+
+func (l Label) X() int {
+	return l.width*5 + 10
+}
+
+type Message struct {
+	text  string
+	width int
+	x     int
+}
+
+func (m Message) Text() string {
+	return m.text
+}
+
+func (m Message) Width() int {
+	return m.width
+}
+
+func (m Message) X() int {
+	return m.x
+}
+
+func (m Message) TextLength() int {
+	return int(float64(m.width-defaultOffset) * 9.5)
+}
+
+type Badge struct {
+	Color    string
+	FontSize int
+	Label    Label
+	Message  Message
+}
+
+func (b Badge) Width() int {
+	return b.Label.width + b.Message.width
+}
+
+const (
+	defaultOffset    = 9
+	defaultFontSize  = 11
+	DefaultColor     = "#9f9f9f" // Grey
+	defaultFontWidth = 7         // approximate speculation
+)
+
+var StatusColorMap = map[actions_model.Status]string{
+	actions_model.StatusSuccess:   "#4c1",    // Green
+	actions_model.StatusSkipped:   "#dfb317", // Yellow
+	actions_model.StatusUnknown:   "#97ca00", // Light Green
+	actions_model.StatusFailure:   "#e05d44", // Red
+	actions_model.StatusCancelled: "#fe7d37", // Orange
+	actions_model.StatusWaiting:   "#dfb317", // Yellow
+	actions_model.StatusRunning:   "#dfb317", // Yellow
+	actions_model.StatusBlocked:   "#dfb317", // Yellow
+}
+
+// GenerateBadge generates badge with given template
+func GenerateBadge(label, message, color string) Badge {
+	lw := defaultFontWidth*len(label) + defaultOffset
+	mw := defaultFontWidth*len(message) + defaultOffset
+	x := lw*10 + mw*5 - 10
+	return Badge{
+		Label: Label{
+			text:  label,
+			width: lw,
+		},
+		Message: Message{
+			text:  message,
+			width: mw,
+			x:     x,
+		},
+		FontSize: defaultFontSize * 10,
+		Color:    color,
+	}
+}
diff --git a/modules/base/natural_sort.go b/modules/base/natural_sort.go
index e920177f89..0f90ec70ce 100644
--- a/modules/base/natural_sort.go
+++ b/modules/base/natural_sort.go
@@ -4,85 +4,12 @@
 package base
 
 import (
-	"math/big"
-	"unicode/utf8"
+	"golang.org/x/text/collate"
+	"golang.org/x/text/language"
 )
 
 // NaturalSortLess compares two strings so that they could be sorted in natural order
 func NaturalSortLess(s1, s2 string) bool {
-	var i1, i2 int
-	for {
-		rune1, j1, end1 := getNextRune(s1, i1)
-		rune2, j2, end2 := getNextRune(s2, i2)
-		if end1 || end2 {
-			return end1 != end2 && end1
-		}
-		dec1 := isDecimal(rune1)
-		dec2 := isDecimal(rune2)
-		var less, equal bool
-		if dec1 && dec2 {
-			i1, i2, less, equal = compareByNumbers(s1, i1, s2, i2)
-		} else if !dec1 && !dec2 {
-			equal = rune1 == rune2
-			less = rune1 < rune2
-			i1 = j1
-			i2 = j2
-		} else {
-			return rune1 < rune2
-		}
-		if !equal {
-			return less
-		}
-	}
-}
-
-func getNextRune(str string, pos int) (rune, int, bool) {
-	if pos < len(str) {
-		r, w := utf8.DecodeRuneInString(str[pos:])
-		// Fallback to ascii
-		if r == utf8.RuneError {
-			r = rune(str[pos])
-			w = 1
-		}
-		return r, pos + w, false
-	}
-	return 0, pos, true
-}
-
-func isDecimal(r rune) bool {
-	return '0' <= r && r <= '9'
-}
-
-func compareByNumbers(str1 string, pos1 int, str2 string, pos2 int) (i1, i2 int, less, equal bool) {
-	d1, d2 := true, true
-	var dec1, dec2 string
-	for d1 || d2 {
-		if d1 {
-			r, j, end := getNextRune(str1, pos1)
-			if !end && isDecimal(r) {
-				dec1 += string(r)
-				pos1 = j
-			} else {
-				d1 = false
-			}
-		}
-		if d2 {
-			r, j, end := getNextRune(str2, pos2)
-			if !end && isDecimal(r) {
-				dec2 += string(r)
-				pos2 = j
-			} else {
-				d2 = false
-			}
-		}
-	}
-	less, equal = compareBigNumbers(dec1, dec2)
-	return pos1, pos2, less, equal
-}
-
-func compareBigNumbers(dec1, dec2 string) (less, equal bool) {
-	d1, _ := big.NewInt(0).SetString(dec1, 10)
-	d2, _ := big.NewInt(0).SetString(dec2, 10)
-	cmp := d1.Cmp(d2)
-	return cmp < 0, cmp == 0
+	c := collate.New(language.English, collate.Numeric)
+	return c.CompareString(s1, s2) < 0
 }
diff --git a/modules/base/natural_sort_test.go b/modules/base/natural_sort_test.go
index 91e864ad2a..f27a4eb53a 100644
--- a/modules/base/natural_sort_test.go
+++ b/modules/base/natural_sort_test.go
@@ -11,7 +11,7 @@ import (
 
 func TestNaturalSortLess(t *testing.T) {
 	test := func(s1, s2 string, less bool) {
-		assert.Equal(t, less, NaturalSortLess(s1, s2))
+		assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2)
 	}
 	test("v1.20.0", "v1.2.0", false)
 	test("v1.20.0", "v1.29.0", true)
@@ -20,4 +20,11 @@ func TestNaturalSortLess(t *testing.T) {
 	test("a-1-a", "a-1-b", true)
 	test("2", "12", true)
 	test("a", "ab", true)
+
+	test("A", "b", true)
+	test("a", "B", true)
+
+	test("cafe", "café", true)
+	test("café", "cafe", false)
+	test("caff", "café", false)
 }
diff --git a/modules/base/tool.go b/modules/base/tool.go
index e9f4dfa279..40785e74e8 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -5,6 +5,7 @@ package base
 
 import (
 	"crypto/sha1"
+	"crypto/sha256"
 	"encoding/base64"
 	"encoding/hex"
 	"errors"
@@ -22,7 +23,6 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/dustin/go-humanize"
-	"github.com/minio/sha256-simd"
 )
 
 // EncodeSha1 string to sha1 hex value.
@@ -115,7 +115,7 @@ func CreateTimeLimitCode(data string, minutes int, startInf any) string {
 
 	// create sha1 encode string
 	sh := sha1.New()
-	_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, setting.SecretKey, startStr, endStr, minutes)))
+	_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, endStr, minutes)))
 	encoded := hex.EncodeToString(sh.Sum(nil))
 
 	code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
@@ -150,13 +150,16 @@ func TruncateString(str string, limit int) string {
 
 // StringsToInt64s converts a slice of string to a slice of int64.
 func StringsToInt64s(strs []string) ([]int64, error) {
-	ints := make([]int64, len(strs))
-	for i := range strs {
-		n, err := strconv.ParseInt(strs[i], 10, 64)
+	if strs == nil {
+		return nil, nil
+	}
+	ints := make([]int64, 0, len(strs))
+	for _, s := range strs {
+		n, err := strconv.ParseInt(s, 10, 64)
 		if err != nil {
-			return ints, err
+			return nil, err
 		}
-		ints[i] = n
+		ints = append(ints, n)
 	}
 	return ints, nil
 }
diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go
index d28deb593d..f21b89c74c 100644
--- a/modules/base/tool_test.go
+++ b/modules/base/tool_test.go
@@ -138,12 +138,13 @@ func TestStringsToInt64s(t *testing.T) {
 		assert.NoError(t, err)
 		assert.Equal(t, expected, result)
 	}
+	testSuccess(nil, nil)
 	testSuccess([]string{}, []int64{})
 	testSuccess([]string{"-1234"}, []int64{-1234})
-	testSuccess([]string{"1", "4", "16", "64", "256"},
-		[]int64{1, 4, 16, 64, 256})
+	testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256})
 
-	_, err := StringsToInt64s([]string{"-1", "a", "$"})
+	ints, err := StringsToInt64s([]string{"-1", "a"})
+	assert.Len(t, ints, 0)
 	assert.Error(t, err)
 }
 
diff --git a/modules/charset/escape_stream.go b/modules/charset/escape_stream.go
index 3f08fd94a4..29943eb858 100644
--- a/modules/charset/escape_stream.go
+++ b/modules/charset/escape_stream.go
@@ -173,7 +173,7 @@ func (e *escapeStreamer) ambiguousRune(r, c rune) error {
 		Val: "ambiguous-code-point",
 	}, html.Attribute{
 		Key: "data-tooltip-content",
-		Val: e.locale.Tr("repo.ambiguous_character", r, c),
+		Val: e.locale.TrString("repo.ambiguous_character", r, c),
 	}); err != nil {
 		return err
 	}
diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go
index a353ced631..9d796a0c18 100644
--- a/modules/charset/escape_test.go
+++ b/modules/charset/escape_test.go
@@ -4,6 +4,7 @@
 package charset
 
 import (
+	"regexp"
 	"strings"
 	"testing"
 
@@ -156,13 +157,16 @@ func TestEscapeControlReader(t *testing.T) {
 		tests = append(tests, test)
 	}
 
+	re := regexp.MustCompile(`repo.ambiguous_character:\d+,\d+`) // simplify the output for the tests, remove the translation variants
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			output := &strings.Builder{}
 			status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{})
 			assert.NoError(t, err)
 			assert.Equal(t, tt.status, *status)
-			assert.Equal(t, tt.result, output.String())
+			outStr := output.String()
+			outStr = re.ReplaceAllString(outStr, "repo.ambiguous_character")
+			assert.Equal(t, tt.result, outStr)
 		})
 	}
 }
diff --git a/modules/container/filter.go b/modules/container/filter.go
new file mode 100644
index 0000000000..37ec7c3d56
--- /dev/null
+++ b/modules/container/filter.go
@@ -0,0 +1,21 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package container
+
+import "slices"
+
+// FilterSlice ranges over the slice and calls include() for each element.
+// If the second returned value is true, the first returned value will be included in the resulting
+// slice (after deduplication).
+func FilterSlice[E any, T comparable](s []E, include func(E) (T, bool)) []T {
+	filtered := make([]T, 0, len(s)) // slice will be clipped before returning
+	seen := make(map[T]bool, len(s))
+	for i := range s {
+		if v, ok := include(s[i]); ok && !seen[v] {
+			filtered = append(filtered, v)
+			seen[v] = true
+		}
+	}
+	return slices.Clip(filtered)
+}
diff --git a/modules/container/filter_test.go b/modules/container/filter_test.go
new file mode 100644
index 0000000000..ad304e5abb
--- /dev/null
+++ b/modules/container/filter_test.go
@@ -0,0 +1,28 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package container
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestFilterMapUnique(t *testing.T) {
+	result := FilterSlice([]int{
+		0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+	}, func(i int) (int, bool) {
+		switch i {
+		case 0:
+			return 0, true // included later
+		case 1:
+			return 0, true // duplicate of previous (should be ignored)
+		case 2:
+			return 2, false // not included
+		default:
+			return i, true
+		}
+	})
+	assert.Equal(t, []int{0, 3, 4, 5, 6, 7, 8, 9}, result)
+}
diff --git a/modules/context/context_test.go b/modules/context/context_test.go
deleted file mode 100644
index 033ce2ef0a..0000000000
--- a/modules/context/context_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package context
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"code.gitea.io/gitea/modules/setting"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestRemoveSessionCookieHeader(t *testing.T) {
-	w := httptest.NewRecorder()
-	w.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String())
-	w.Header().Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String())
-	assert.Len(t, w.Header().Values("Set-Cookie"), 2)
-	removeSessionCookieHeader(w)
-	assert.Len(t, w.Header().Values("Set-Cookie"), 1)
-	assert.Contains(t, "other=bar", w.Header().Get("Set-Cookie"))
-}
diff --git a/modules/csv/csv.go b/modules/csv/csv.go
index c5497befe7..35c5d6ab67 100644
--- a/modules/csv/csv.go
+++ b/modules/csv/csv.go
@@ -123,9 +123,9 @@ func guessDelimiter(data []byte) rune {
 func FormatError(err error, locale translation.Locale) (string, error) {
 	if perr, ok := err.(*stdcsv.ParseError); ok {
 		if perr.Err == stdcsv.ErrFieldCount {
-			return locale.Tr("repo.error.csv.invalid_field_count", perr.Line), nil
+			return locale.TrString("repo.error.csv.invalid_field_count", perr.Line), nil
 		}
-		return locale.Tr("repo.error.csv.unexpected", perr.Line, perr.Column), nil
+		return locale.TrString("repo.error.csv.unexpected", perr.Line, perr.Column), nil
 	}
 
 	return "", err
diff --git a/modules/csv/csv_test.go b/modules/csv/csv_test.go
index f6e782a5a4..3ddb47acbb 100644
--- a/modules/csv/csv_test.go
+++ b/modules/csv/csv_test.go
@@ -561,14 +561,14 @@ func TestFormatError(t *testing.T) {
 			err: &csv.ParseError{
 				Err: csv.ErrFieldCount,
 			},
-			expectedMessage: "repo.error.csv.invalid_field_count",
+			expectedMessage: "repo.error.csv.invalid_field_count:0",
 			expectsError:    false,
 		},
 		{
 			err: &csv.ParseError{
 				Err: csv.ErrBareQuote,
 			},
-			expectedMessage: "repo.error.csv.unexpected",
+			expectedMessage: "repo.error.csv.unexpected:0,0",
 			expectsError:    false,
 		},
 		{
diff --git a/modules/dump/dumper.go b/modules/dump/dumper.go
new file mode 100644
index 0000000000..47730851fb
--- /dev/null
+++ b/modules/dump/dumper.go
@@ -0,0 +1,174 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package dump
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"path"
+	"path/filepath"
+	"slices"
+	"strings"
+
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/timeutil"
+
+	"github.com/mholt/archiver/v3"
+)
+
+var SupportedOutputTypes = []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"}
+
+// PrepareFileNameAndType prepares the output file name and type, if the type is not supported, it returns an empty "outType"
+func PrepareFileNameAndType(argFile, argType string) (outFileName, outType string) {
+	if argFile == "" && argType == "" {
+		outType = SupportedOutputTypes[0]
+		outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
+	} else if argFile == "" {
+		outType = argType
+		outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
+	} else if argType == "" {
+		if filepath.Ext(outFileName) == "" {
+			outType = SupportedOutputTypes[0]
+			outFileName = argFile
+		} else {
+			for _, t := range SupportedOutputTypes {
+				if strings.HasSuffix(argFile, "."+t) {
+					outFileName = argFile
+					outType = t
+				}
+			}
+		}
+	} else {
+		outFileName, outType = argFile, argType
+	}
+	if !slices.Contains(SupportedOutputTypes, outType) {
+		return "", ""
+	}
+	return outFileName, outType
+}
+
+func IsSubdir(upper, lower string) (bool, error) {
+	if relPath, err := filepath.Rel(upper, lower); err != nil {
+		return false, err
+	} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
+		return true, nil
+	}
+	return false, nil
+}
+
+type Dumper struct {
+	Writer  archiver.Writer
+	Verbose bool
+
+	globalExcludeAbsPaths []string
+}
+
+func (dumper *Dumper) AddReader(r io.ReadCloser, info os.FileInfo, customName string) error {
+	if dumper.Verbose {
+		log.Info("Adding file %s", customName)
+	}
+
+	return dumper.Writer.Write(archiver.File{
+		FileInfo: archiver.FileInfo{
+			FileInfo:   info,
+			CustomName: customName,
+		},
+		ReadCloser: r,
+	})
+}
+
+func (dumper *Dumper) AddFile(filePath, absPath string) error {
+	file, err := os.Open(absPath)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	fileInfo, err := file.Stat()
+	if err != nil {
+		return err
+	}
+	return dumper.AddReader(file, fileInfo, filePath)
+}
+
+func (dumper *Dumper) normalizeFilePath(absPath string) string {
+	absPath = filepath.Clean(absPath)
+	if setting.IsWindows {
+		absPath = strings.ToLower(absPath)
+	}
+	return absPath
+}
+
+func (dumper *Dumper) GlobalExcludeAbsPath(absPaths ...string) {
+	for _, absPath := range absPaths {
+		dumper.globalExcludeAbsPaths = append(dumper.globalExcludeAbsPaths, dumper.normalizeFilePath(absPath))
+	}
+}
+
+func (dumper *Dumper) shouldExclude(absPath string, excludes []string) bool {
+	norm := dumper.normalizeFilePath(absPath)
+	return slices.Contains(dumper.globalExcludeAbsPaths, norm) || slices.Contains(excludes, norm)
+}
+
+func (dumper *Dumper) AddRecursiveExclude(insidePath, absPath string, excludes []string) error {
+	excludes = slices.Clone(excludes)
+	for i := range excludes {
+		excludes[i] = dumper.normalizeFilePath(excludes[i])
+	}
+	return dumper.addFileOrDir(insidePath, absPath, excludes)
+}
+
+func (dumper *Dumper) addFileOrDir(insidePath, absPath string, excludes []string) error {
+	absPath, err := filepath.Abs(absPath)
+	if err != nil {
+		return err
+	}
+	dir, err := os.Open(absPath)
+	if err != nil {
+		return err
+	}
+	defer dir.Close()
+
+	files, err := dir.Readdir(0)
+	if err != nil {
+		return err
+	}
+	for _, file := range files {
+		currentAbsPath := filepath.Join(absPath, file.Name())
+		if dumper.shouldExclude(currentAbsPath, excludes) {
+			continue
+		}
+
+		currentInsidePath := path.Join(insidePath, file.Name())
+		if file.IsDir() {
+			if err := dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
+				return err
+			}
+			if err = dumper.addFileOrDir(currentInsidePath, currentAbsPath, excludes); err != nil {
+				return err
+			}
+		} else {
+			// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
+			shouldAdd := file.Mode().IsRegular()
+			if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
+				target, err := filepath.EvalSymlinks(currentAbsPath)
+				if err != nil {
+					return err
+				}
+				targetStat, err := os.Stat(target)
+				if err != nil {
+					return err
+				}
+				shouldAdd = targetStat.Mode().IsRegular()
+			}
+			if shouldAdd {
+				if err = dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
+					return err
+				}
+			}
+		}
+	}
+	return nil
+}
diff --git a/modules/dump/dumper_test.go b/modules/dump/dumper_test.go
new file mode 100644
index 0000000000..b444fa2de5
--- /dev/null
+++ b/modules/dump/dumper_test.go
@@ -0,0 +1,113 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package dump
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"sort"
+	"testing"
+	"time"
+
+	"code.gitea.io/gitea/modules/timeutil"
+
+	"github.com/mholt/archiver/v3"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPrepareFileNameAndType(t *testing.T) {
+	defer timeutil.MockSet(time.Unix(1234, 0))()
+	test := func(argFile, argType, expFile, expType string) {
+		outFile, outType := PrepareFileNameAndType(argFile, argType)
+		assert.Equal(t,
+			fmt.Sprintf("outFile=%s, outType=%s", expFile, expType),
+			fmt.Sprintf("outFile=%s, outType=%s", outFile, outType),
+			fmt.Sprintf("argFile=%s, argType=%s", argFile, argType),
+		)
+	}
+
+	test("", "", "gitea-dump-1234.zip", "zip")
+	test("", "tar.gz", "gitea-dump-1234.tar.gz", "tar.gz")
+	test("", "no-such", "", "")
+
+	test("-", "", "-", "zip")
+	test("-", "tar.gz", "-", "tar.gz")
+	test("-", "no-such", "", "")
+
+	test("a", "", "a", "zip")
+	test("a", "tar.gz", "a", "tar.gz")
+	test("a", "no-such", "", "")
+
+	test("a.zip", "", "a.zip", "zip")
+	test("a.zip", "tar.gz", "a.zip", "tar.gz")
+	test("a.zip", "no-such", "", "")
+
+	test("a.tar.gz", "", "a.tar.gz", "zip")
+	test("a.tar.gz", "tar.gz", "a.tar.gz", "tar.gz")
+	test("a.tar.gz", "no-such", "", "")
+}
+
+func TestIsSubDir(t *testing.T) {
+	tmpDir := t.TempDir()
+	_ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755)
+
+	isSub, err := IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include"))
+	assert.NoError(t, err)
+	assert.True(t, isSub)
+
+	isSub, err = IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include/sub"))
+	assert.NoError(t, err)
+	assert.True(t, isSub)
+
+	isSub, err = IsSubdir(filepath.Join(tmpDir, "include/sub"), filepath.Join(tmpDir, "include"))
+	assert.NoError(t, err)
+	assert.False(t, isSub)
+}
+
+type testWriter struct {
+	added []string
+}
+
+func (t *testWriter) Create(out io.Writer) error {
+	return nil
+}
+
+func (t *testWriter) Write(f archiver.File) error {
+	t.added = append(t.added, f.Name())
+	return nil
+}
+
+func (t *testWriter) Close() error {
+	return nil
+}
+
+func TestDumper(t *testing.T) {
+	sortStrings := func(s []string) []string {
+		sort.Strings(s)
+		return s
+	}
+	tmpDir := t.TempDir()
+	_ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude1"), 0o755)
+	_ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude2"), 0o755)
+	_ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755)
+	_ = os.WriteFile(filepath.Join(tmpDir, "include/a"), nil, 0o644)
+	_ = os.WriteFile(filepath.Join(tmpDir, "include/sub/b"), nil, 0o644)
+	_ = os.WriteFile(filepath.Join(tmpDir, "include/exclude1/a-1"), nil, 0o644)
+	_ = os.WriteFile(filepath.Join(tmpDir, "include/exclude2/a-2"), nil, 0o644)
+
+	tw := &testWriter{}
+	d := &Dumper{Writer: tw}
+	d.GlobalExcludeAbsPath(filepath.Join(tmpDir, "include/exclude1"))
+	err := d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), []string{filepath.Join(tmpDir, "include/exclude2")})
+	assert.NoError(t, err)
+	assert.EqualValues(t, sortStrings([]string{"include/a", "include/sub", "include/sub/b"}), sortStrings(tw.added))
+
+	tw = &testWriter{}
+	d = &Dumper{Writer: tw}
+	err = d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), nil)
+	assert.NoError(t, err)
+	assert.EqualValues(t, sortStrings([]string{"include/exclude2", "include/exclude2/a-2", "include/a", "include/sub", "include/sub/b", "include/exclude1", "include/exclude1/a-1"}), sortStrings(tw.added))
+}
diff --git a/modules/generate/generate.go b/modules/generate/generate.go
index ee3c76059b..2d9a3dd902 100644
--- a/modules/generate/generate.go
+++ b/modules/generate/generate.go
@@ -7,6 +7,7 @@ package generate
 import (
 	"crypto/rand"
 	"encoding/base64"
+	"fmt"
 	"io"
 	"time"
 
@@ -38,19 +39,24 @@ func NewInternalToken() (string, error) {
 	return internalToken, nil
 }
 
-// NewJwtSecret generates a new value intended to be used for JWT secrets.
-func NewJwtSecret() ([]byte, error) {
-	bytes := make([]byte, 32)
-	_, err := io.ReadFull(rand.Reader, bytes)
-	if err != nil {
+const defaultJwtSecretLen = 32
+
+// DecodeJwtSecretBase64 decodes a base64 encoded jwt secret into bytes, and check its length
+func DecodeJwtSecretBase64(src string) ([]byte, error) {
+	encoding := base64.RawURLEncoding
+	decoded := make([]byte, encoding.DecodedLen(len(src))+3)
+	if n, err := encoding.Decode(decoded, []byte(src)); err != nil {
 		return nil, err
+	} else if n != defaultJwtSecretLen {
+		return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, defaultJwtSecretLen)
 	}
-	return bytes, nil
+	return decoded[:defaultJwtSecretLen], nil
 }
 
-// NewJwtSecretBase64 generates a new base64 encoded value intended to be used for JWT secrets.
-func NewJwtSecretBase64() ([]byte, string, error) {
-	bytes, err := NewJwtSecret()
+// NewJwtSecretWithBase64 generates a jwt secret with its base64 encoded value intended to be used for saving into config file
+func NewJwtSecretWithBase64() ([]byte, string, error) {
+	bytes := make([]byte, defaultJwtSecretLen)
+	_, err := io.ReadFull(rand.Reader, bytes)
 	if err != nil {
 		return nil, "", err
 	}
diff --git a/modules/generate/generate_test.go b/modules/generate/generate_test.go
new file mode 100644
index 0000000000..af640a60c1
--- /dev/null
+++ b/modules/generate/generate_test.go
@@ -0,0 +1,34 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package generate
+
+import (
+	"encoding/base64"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDecodeJwtSecretBase64(t *testing.T) {
+	_, err := DecodeJwtSecretBase64("abcd")
+	assert.ErrorContains(t, err, "invalid base64 decoded length")
+	_, err = DecodeJwtSecretBase64(strings.Repeat("a", 64))
+	assert.ErrorContains(t, err, "invalid base64 decoded length")
+
+	str32 := strings.Repeat("x", 32)
+	encoded32 := base64.RawURLEncoding.EncodeToString([]byte(str32))
+	decoded32, err := DecodeJwtSecretBase64(encoded32)
+	assert.NoError(t, err)
+	assert.Equal(t, str32, string(decoded32))
+}
+
+func TestNewJwtSecretWithBase64(t *testing.T) {
+	secret, encoded, err := NewJwtSecretWithBase64()
+	assert.NoError(t, err)
+	assert.Len(t, secret, 32)
+	decoded, err := DecodeJwtSecretBase64(encoded)
+	assert.NoError(t, err)
+	assert.Equal(t, secret, decoded)
+}
diff --git a/modules/git/attribute.go b/modules/git/attribute.go
new file mode 100644
index 0000000000..4dfa510369
--- /dev/null
+++ b/modules/git/attribute.go
@@ -0,0 +1,35 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"code.gitea.io/gitea/modules/optional"
+)
+
+const (
+	AttributeLinguistVendored      = "linguist-vendored"
+	AttributeLinguistGenerated     = "linguist-generated"
+	AttributeLinguistDocumentation = "linguist-documentation"
+	AttributeLinguistDetectable    = "linguist-detectable"
+	AttributeLinguistLanguage      = "linguist-language"
+	AttributeGitlabLanguage        = "gitlab-language"
+)
+
+// true if "set"/"true", false if "unset"/"false", none otherwise
+func AttributeToBool(attr map[string]string, name string) optional.Option[bool] {
+	switch attr[name] {
+	case "set", "true":
+		return optional.Some(true)
+	case "unset", "false":
+		return optional.Some(false)
+	}
+	return optional.None[bool]()
+}
+
+func AttributeToString(attr map[string]string, name string) optional.Option[string] {
+	if value, has := attr[name]; has && value != "unspecified" {
+		return optional.Some(value)
+	}
+	return optional.None[string]()
+}
diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go
index 53a9393d5f..043dbb44bd 100644
--- a/modules/git/batch_reader.go
+++ b/modules/git/batch_reader.go
@@ -203,16 +203,7 @@ headerLoop:
 	}
 
 	// Discard the rest of the tag
-	discard := size - n + 1
-	for discard > math.MaxInt32 {
-		_, err := rd.Discard(math.MaxInt32)
-		if err != nil {
-			return id, err
-		}
-		discard -= math.MaxInt32
-	}
-	_, err := rd.Discard(int(discard))
-	return id, err
+	return id, DiscardFull(rd, size-n+1)
 }
 
 // ReadTreeID reads a tree ID from a cat-file --batch stream, throwing away the rest of the stream.
@@ -238,16 +229,7 @@ headerLoop:
 	}
 
 	// Discard the rest of the commit
-	discard := size - n + 1
-	for discard > math.MaxInt32 {
-		_, err := rd.Discard(math.MaxInt32)
-		if err != nil {
-			return id, err
-		}
-		discard -= math.MaxInt32
-	}
-	_, err := rd.Discard(int(discard))
-	return id, err
+	return id, DiscardFull(rd, size-n+1)
 }
 
 // git tree files are a list:
@@ -345,3 +327,21 @@ func init() {
 	_, filename, _, _ := runtime.Caller(0)
 	callerPrefix = strings.TrimSuffix(filename, "modules/git/batch_reader.go")
 }
+
+func DiscardFull(rd *bufio.Reader, discard int64) error {
+	if discard > math.MaxInt32 {
+		n, err := rd.Discard(math.MaxInt32)
+		discard -= int64(n)
+		if err != nil {
+			return err
+		}
+	}
+	for discard > 0 {
+		n, err := rd.Discard(int(discard))
+		discard -= int64(n)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/modules/git/blame.go b/modules/git/blame.go
index 64095a218a..69e1b08f93 100644
--- a/modules/git/blame.go
+++ b/modules/git/blame.go
@@ -115,6 +115,10 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
 
 // Close BlameReader - don't run NextPart after invoking that
 func (r *BlameReader) Close() error {
+	if r.bufferedReader == nil {
+		return nil
+	}
+
 	err := <-r.done
 	r.bufferedReader = nil
 	_ = r.reader.Close()
diff --git a/modules/git/blame_sha256_test.go b/modules/git/blame_sha256_test.go
index 01de0454a3..8cd345714f 100644
--- a/modules/git/blame_sha256_test.go
+++ b/modules/git/blame_sha256_test.go
@@ -118,11 +118,12 @@ func TestReadingBlameOutputSha256(t *testing.T) {
 			},
 		}
 
+		objectFormat, err := repo.GetObjectFormat()
+		assert.NoError(t, err)
 		for _, c := range cases {
 			commit, err := repo.GetCommit(c.CommitID)
 			assert.NoError(t, err)
-
-			blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass)
+			blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass)
 			assert.NoError(t, err)
 			assert.NotNil(t, blameReader)
 			defer blameReader.Close()
diff --git a/modules/git/blame_test.go b/modules/git/blame_test.go
index 327edab767..4220c85600 100644
--- a/modules/git/blame_test.go
+++ b/modules/git/blame_test.go
@@ -118,11 +118,13 @@ func TestReadingBlameOutput(t *testing.T) {
 			},
 		}
 
+		objectFormat, err := repo.GetObjectFormat()
+		assert.NoError(t, err)
 		for _, c := range cases {
 			commit, err := repo.GetCommit(c.CommitID)
 			assert.NoError(t, err)
 
-			blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass)
+			blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass)
 			assert.NoError(t, err)
 			assert.NotNil(t, blameReader)
 			defer blameReader.Close()
diff --git a/modules/git/blob_nogogit.go b/modules/git/blob_nogogit.go
index 6e8a48b1db..945a6bc432 100644
--- a/modules/git/blob_nogogit.go
+++ b/modules/git/blob_nogogit.go
@@ -9,7 +9,6 @@ import (
 	"bufio"
 	"bytes"
 	"io"
-	"math"
 
 	"code.gitea.io/gitea/modules/log"
 )
@@ -103,26 +102,17 @@ func (b *blobReader) Read(p []byte) (n int, err error) {
 
 // Close implements io.Closer
 func (b *blobReader) Close() error {
-	defer b.cancel()
-	if b.n > 0 {
-		for b.n > math.MaxInt32 {
-			n, err := b.rd.Discard(math.MaxInt32)
-			b.n -= int64(n)
-			if err != nil {
-				return err
-			}
-			b.n -= math.MaxInt32
-		}
-		n, err := b.rd.Discard(int(b.n))
-		b.n -= int64(n)
-		if err != nil {
-			return err
-		}
+	if b.rd == nil {
+		return nil
 	}
-	if b.n == 0 {
-		_, err := b.rd.Discard(1)
-		b.n--
+
+	defer b.cancel()
+
+	if err := DiscardFull(b.rd, b.n+1); err != nil {
 		return err
 	}
+
+	b.rd = nil
+
 	return nil
 }
diff --git a/modules/git/command.go b/modules/git/command.go
index 9305ef6f92..22cb275ab2 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -12,6 +12,7 @@ import (
 	"io"
 	"os"
 	"os/exec"
+	"runtime"
 	"strings"
 	"time"
 
@@ -344,6 +345,17 @@ func (c *Command) Run(opts *RunOpts) error {
 		log.Debug("slow git.Command.Run: %s (%s)", c, elapsed)
 	}
 
+	// We need to check if the context is canceled by the program on Windows.
+	// This is because Windows does not have signal checking when terminating the process.
+	// It always returns exit code 1, unlike Linux, which has many exit codes for signals.
+	if runtime.GOOS == "windows" &&
+		err != nil &&
+		err.Error() == "" &&
+		cmd.ProcessState.ExitCode() == 1 &&
+		ctx.Err() == context.Canceled {
+		return ctx.Err()
+	}
+
 	if err != nil && ctx.Err() != context.DeadlineExceeded {
 		return err
 	}
@@ -355,7 +367,6 @@ type RunStdError interface {
 	error
 	Unwrap() error
 	Stderr() string
-	IsExitCode(code int) bool
 }
 
 type runStdError struct {
@@ -380,9 +391,9 @@ func (r *runStdError) Stderr() string {
 	return r.stderr
 }
 
-func (r *runStdError) IsExitCode(code int) bool {
+func IsErrorExitCode(err error, code int) bool {
 	var exitError *exec.ExitError
-	if errors.As(r.err, &exitError) {
+	if errors.As(err, &exitError) {
 		return exitError.ExitCode() == code
 	}
 	return false
diff --git a/modules/git/commit.go b/modules/git/commit.go
index 5d960e92f3..5f442b0e1a 100644
--- a/modules/git/commit.go
+++ b/modules/git/commit.go
@@ -9,6 +9,7 @@ import (
 	"bytes"
 	"context"
 	"errors"
+	"fmt"
 	"io"
 	"os/exec"
 	"strconv"
@@ -25,14 +26,14 @@ type Commit struct {
 	Author        *Signature
 	Committer     *Signature
 	CommitMessage string
-	Signature     *CommitGPGSignature
+	Signature     *CommitSignature
 
 	Parents        []ObjectID // ID strings
 	submoduleCache *ObjectCache
 }
 
-// CommitGPGSignature represents a git commit signature part.
-type CommitGPGSignature struct {
+// CommitSignature represents a git commit signature part.
+type CommitSignature struct {
 	Signature string
 	Payload   string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data
 }
@@ -311,7 +312,7 @@ func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error)
 	return c.repo.GetFilesChangedBetween(pastCommit, c.ID.String())
 }
 
-// FileChangedSinceCommit Returns true if the file given has changed since the the past commit
+// FileChangedSinceCommit Returns true if the file given has changed since the past commit
 // YOU MUST ENSURE THAT pastCommit is a valid commit ID.
 func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) {
 	return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String())
@@ -396,6 +397,9 @@ func (c *Commit) GetSubModules() (*ObjectCache, error) {
 			}
 		}
 	}
+	if err = scanner.Err(); err != nil {
+		return nil, fmt.Errorf("GetSubModules scan: %w", err)
+	}
 
 	return c.submoduleCache, nil
 }
diff --git a/modules/git/commit_convert_gogit.go b/modules/git/commit_convert_gogit.go
index 819ea0d1db..d7b945ed6b 100644
--- a/modules/git/commit_convert_gogit.go
+++ b/modules/git/commit_convert_gogit.go
@@ -13,7 +13,7 @@ import (
 	"github.com/go-git/go-git/v5/plumbing/object"
 )
 
-func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
+func convertPGPSignature(c *object.Commit) *CommitSignature {
 	if c.PGPSignature == "" {
 		return nil
 	}
@@ -47,11 +47,17 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
 		return nil
 	}
 
+	if c.Encoding != "" && c.Encoding != "UTF-8" {
+		if _, err = fmt.Fprintf(&w, "\nencoding %s\n", c.Encoding); err != nil {
+			return nil
+		}
+	}
+
 	if _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil {
 		return nil
 	}
 
-	return &CommitGPGSignature{
+	return &CommitSignature{
 		Signature: c.PGPSignature,
 		Payload:   w.String(),
 	}
diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go
index e469d2cab6..a5d18694f7 100644
--- a/modules/git/commit_info_nogogit.go
+++ b/modules/git/commit_info_nogogit.go
@@ -151,6 +151,9 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string,
 			return nil, err
 		}
 		if typ != "commit" {
+			if err := DiscardFull(batchReader, size+1); err != nil {
+				return nil, err
+			}
 			return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID)
 		}
 		c, err = CommitFromReader(commit.repo, MustIDFromString(commitID), io.LimitReader(batchReader, size))
diff --git a/modules/git/commit_reader.go b/modules/git/commit_reader.go
index 4809d6c7ed..f1f4a0e588 100644
--- a/modules/git/commit_reader.go
+++ b/modules/git/commit_reader.go
@@ -84,6 +84,8 @@ readLoop:
 				commit.Committer = &Signature{}
 				commit.Committer.Decode(data)
 				_, _ = payloadSB.Write(line)
+			case "encoding":
+				_, _ = payloadSB.Write(line)
 			case "gpgsig":
 				fallthrough
 			case "gpgsig-sha256": // FIXME: no intertop, so only 1 exists at present.
@@ -97,7 +99,7 @@ readLoop:
 		}
 	}
 	commit.CommitMessage = messageSB.String()
-	commit.Signature = &CommitGPGSignature{
+	commit.Signature = &CommitSignature{
 		Signature: signatureSB.String(),
 		Payload:   payloadSB.String(),
 	}
diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go
index 82112cb409..3b8b6d3763 100644
--- a/modules/git/commit_sha256_test.go
+++ b/modules/git/commit_sha256_test.go
@@ -140,10 +140,13 @@ func TestHasPreviousCommitSha256(t *testing.T) {
 	commit, err := repo.GetCommit("f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc")
 	assert.NoError(t, err)
 
+	objectFormat, err := repo.GetObjectFormat()
+	assert.NoError(t, err)
+
 	parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c")
 	notParentSHA := MustIDFromString("42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236")
-	assert.Equal(t, repo.objectFormat, parentSHA.Type())
-	assert.Equal(t, repo.objectFormat.Name(), "sha256")
+	assert.Equal(t, objectFormat, parentSHA.Type())
+	assert.Equal(t, objectFormat.Name(), "sha256")
 
 	haz, err := commit.HasPreviousCommit(parentSHA)
 	assert.NoError(t, err)
diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go
index e512eecc56..a33e7df31a 100644
--- a/modules/git/commit_test.go
+++ b/modules/git/commit_test.go
@@ -125,6 +125,73 @@ empty commit`, commitFromReader.Signature.Payload)
 	assert.EqualValues(t, commitFromReader, commitFromReader2)
 }
 
+func TestCommitWithEncodingFromReader(t *testing.T) {
+	commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074
+tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
+parent 47b24e7ab977ed31c5a39989d570847d6d0052af
+author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
+committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
+encoding ISO-8859-1
+gpgsig -----BEGIN PGP SIGNATURE-----
+ 
+ iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow
+ Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR
+ gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq
+ zOfZraLOEWRH4tZcS+u2yFLu3ez2Wqh1xW5LNy7xqEedMXEFD1HwSJ0+pjacNkzr
+ frp6Asyt7xRI6YmgFJZJoRsS3Ktr6rtKeRL2IErSQQyorOqj6gKrglhrhfG/114j
+ FKB1v4or0WZ1DE8iP2SJZ3n+/K1IuWAINh7MVdb7PndfBPEa+IL+ucNk5uzEE8Jd
+ G8smGxXUeFEt2cP1dj2W8EgAxuA9sTnH9dqI5aRqy5ifDjuya7Emm8sdOUvtGdmn
+ SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F
+ yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz
+ jw4YcO5u
+ =r3UU
+ -----END PGP SIGNATURE-----
+
+ISO-8859-1`
+
+	sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2}
+	gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare"))
+	assert.NoError(t, err)
+	assert.NotNil(t, gitRepo)
+	defer gitRepo.Close()
+
+	commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString))
+	assert.NoError(t, err)
+	if !assert.NotNil(t, commitFromReader) {
+		return
+	}
+	assert.EqualValues(t, sha, commitFromReader.ID)
+	assert.EqualValues(t, `-----BEGIN PGP SIGNATURE-----
+
+iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow
+Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR
+gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq
+zOfZraLOEWRH4tZcS+u2yFLu3ez2Wqh1xW5LNy7xqEedMXEFD1HwSJ0+pjacNkzr
+frp6Asyt7xRI6YmgFJZJoRsS3Ktr6rtKeRL2IErSQQyorOqj6gKrglhrhfG/114j
+FKB1v4or0WZ1DE8iP2SJZ3n+/K1IuWAINh7MVdb7PndfBPEa+IL+ucNk5uzEE8Jd
+G8smGxXUeFEt2cP1dj2W8EgAxuA9sTnH9dqI5aRqy5ifDjuya7Emm8sdOUvtGdmn
+SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F
+yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz
+jw4YcO5u
+=r3UU
+-----END PGP SIGNATURE-----
+`, commitFromReader.Signature.Signature)
+	assert.EqualValues(t, `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
+parent 47b24e7ab977ed31c5a39989d570847d6d0052af
+author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
+committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
+encoding ISO-8859-1
+
+ISO-8859-1`, commitFromReader.Signature.Payload)
+	assert.EqualValues(t, "KN4CK3R <admin@oldschoolhack.me>", commitFromReader.Author.String())
+
+	commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n"))
+	assert.NoError(t, err)
+	commitFromReader.CommitMessage += "\n\n"
+	commitFromReader.Signature.Payload += "\n\n"
+	assert.EqualValues(t, commitFromReader, commitFromReader2)
+}
+
 func TestHasPreviousCommit(t *testing.T) {
 	bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
 
diff --git a/modules/git/error.go b/modules/git/error.go
index dc10d451b3..91d25eca69 100644
--- a/modules/git/error.go
+++ b/modules/git/error.go
@@ -96,7 +96,7 @@ func (err ErrBranchNotExist) Unwrap() error {
 	return util.ErrNotExist
 }
 
-// ErrPushOutOfDate represents an error if merging fails due to unrelated histories
+// ErrPushOutOfDate represents an error if merging fails due to the base branch being updated
 type ErrPushOutOfDate struct {
 	StdOut string
 	StdErr string
diff --git a/modules/git/git.go b/modules/git/git.go
index 2a1ccab499..6afacaf117 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -33,42 +33,45 @@ var (
 	// DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx
 	DefaultContext context.Context
 
-	SupportProcReceive bool // >= 2.29
-	SupportHashSha256  bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
+	DefaultFeatures struct {
+		GitVersion *version.Version
 
-	gitVersion *version.Version
+		SupportProcReceive bool // >= 2.29
+		SupportHashSha256  bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
+	}
 )
 
-// loadGitVersion returns current Git version from shell. Internal usage only.
-func loadGitVersion() (*version.Version, error) {
+// loadGitVersion tries to get the current git version and stores it into a global variable
+func loadGitVersion() error {
 	// doesn't need RWMutex because it's executed by Init()
-	if gitVersion != nil {
-		return gitVersion, nil
+	if DefaultFeatures.GitVersion != nil {
+		return nil
 	}
 
 	stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil)
 	if runErr != nil {
-		return nil, runErr
+		return runErr
 	}
 
-	fields := strings.Fields(stdout)
+	ver, err := parseGitVersionLine(strings.TrimSpace(stdout))
+	if err == nil {
+		DefaultFeatures.GitVersion = ver
+	}
+	return err
+}
+
+func parseGitVersionLine(s string) (*version.Version, error) {
+	fields := strings.Fields(s)
 	if len(fields) < 3 {
-		return nil, fmt.Errorf("invalid git version output: %s", stdout)
+		return nil, fmt.Errorf("invalid git version: %q", s)
 	}
 
-	var versionString string
-
-	// Handle special case on Windows.
-	i := strings.Index(fields[2], "windows")
-	if i >= 1 {
-		versionString = fields[2][:i-1]
-	} else {
-		versionString = fields[2]
+	// version string is like: "git version 2.29.3" or "git version 2.29.3.windows.1"
+	versionString := fields[2]
+	if pos := strings.Index(versionString, "windows"); pos >= 1 {
+		versionString = versionString[:pos-1]
 	}
-
-	var err error
-	gitVersion, err = version.NewVersion(versionString)
-	return gitVersion, err
+	return version.NewVersion(versionString)
 }
 
 // SetExecutablePath changes the path of git executable and checks the file permission and version.
@@ -83,8 +86,7 @@ func SetExecutablePath(path string) error {
 	}
 	GitExecutable = absPath
 
-	_, err = loadGitVersion()
-	if err != nil {
+	if err = loadGitVersion(); err != nil {
 		return fmt.Errorf("unable to load git version: %w", err)
 	}
 
@@ -93,7 +95,7 @@ func SetExecutablePath(path string) error {
 		return err
 	}
 
-	if gitVersion.LessThan(versionRequired) {
+	if DefaultFeatures.GitVersion.LessThan(versionRequired) {
 		moreHint := "get git: https://git-scm.com/download/"
 		if runtime.GOOS == "linux" {
 			// there are a lot of CentOS/RHEL users using old git, so we add a special hint for them
@@ -102,19 +104,22 @@ func SetExecutablePath(path string) error {
 				moreHint = "get git: https://git-scm.com/download/linux and https://ius.io"
 			}
 		}
-		return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", gitVersion.Original(), RequiredVersion, moreHint)
+		return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", DefaultFeatures.GitVersion.Original(), RequiredVersion, moreHint)
 	}
 
+	if err = checkGitVersionCompatibility(DefaultFeatures.GitVersion); err != nil {
+		return fmt.Errorf("installed git version %s has a known compatibility issue with Gitea: %w, please upgrade (or downgrade) git", DefaultFeatures.GitVersion.String(), err)
+	}
 	return nil
 }
 
 // VersionInfo returns git version information
 func VersionInfo() string {
-	if gitVersion == nil {
+	if DefaultFeatures.GitVersion == nil {
 		return "(git not found)"
 	}
 	format := "%s"
-	args := []any{gitVersion.Original()}
+	args := []any{DefaultFeatures.GitVersion.Original()}
 	// Since git wire protocol has been released from git v2.18
 	if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
 		format += ", Wire Protocol %s Enabled"
@@ -184,9 +189,9 @@ func InitFull(ctx context.Context) (err error) {
 	if CheckGitVersionAtLeast("2.9") == nil {
 		globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
 	}
-	SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
-	SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit
-	if SupportHashSha256 {
+	DefaultFeatures.SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
+	DefaultFeatures.SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit
+	if DefaultFeatures.SupportHashSha256 {
 		SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat)
 	} else {
 		log.Warn("sha256 hash support is disabled - requires Git >= 2.42. Gogit is currently unsupported")
@@ -251,7 +256,7 @@ func syncGitConfig() (err error) {
 		}
 	}
 
-	if SupportProcReceive {
+	if DefaultFeatures.SupportProcReceive {
 		// set support for AGit flow
 		if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil {
 			return err
@@ -262,19 +267,18 @@ func syncGitConfig() (err error) {
 		}
 	}
 
-	// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user
-	// however, some docker users and samba users find it difficult to configure their systems so that Gitea's git repositories are owned by the Gitea user. (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.)
-	// see issue: https://github.com/go-gitea/gitea/issues/19455
-	// Fundamentally the problem lies with the uid-gid-mapping mechanism for filesystems in docker on windows (and to a lesser extent samba).
-	// Docker's configuration mechanism for local filesystems provides no way of setting this mapping and although there is a mechanism for setting this uid through using cifs mounting it is complicated and essentially undocumented
-	// Thus the owner uid/gid for files on these filesystems will be marked as root.
+	// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user.
+	// However, some docker users and samba users find it difficult to configure their systems correctly,
+	// so that Gitea's git repositories are owned by the Gitea user.
+	// (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.)
+	// See issue: https://github.com/go-gitea/gitea/issues/19455
 	// As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea,
 	// it is now safe to set "safe.directory=*" for internal usage only.
-	// Please note: the wildcard "*" is only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later
-	// Although only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later - this setting is tolerated by earlier versions
+	// Although this setting is only supported by some new git versions, it is also tolerated by earlier versions
 	if err := configAddNonExist("safe.directory", "*"); err != nil {
 		return err
 	}
+
 	if runtime.GOOS == "windows" {
 		if err := configSet("core.longpaths", "true"); err != nil {
 			return err
@@ -307,22 +311,37 @@ func syncGitConfig() (err error) {
 
 // CheckGitVersionAtLeast check git version is at least the constraint version
 func CheckGitVersionAtLeast(atLeast string) error {
-	if _, err := loadGitVersion(); err != nil {
-		return err
+	if DefaultFeatures.GitVersion == nil {
+		panic("git module is not initialized") // it shouldn't happen
 	}
 	atLeastVersion, err := version.NewVersion(atLeast)
 	if err != nil {
 		return err
 	}
-	if gitVersion.Compare(atLeastVersion) < 0 {
-		return fmt.Errorf("installed git binary version %s is not at least %s", gitVersion.Original(), atLeast)
+	if DefaultFeatures.GitVersion.Compare(atLeastVersion) < 0 {
+		return fmt.Errorf("installed git binary version %s is not at least %s", DefaultFeatures.GitVersion.Original(), atLeast)
+	}
+	return nil
+}
+
+func checkGitVersionCompatibility(gitVer *version.Version) error {
+	badVersions := []struct {
+		Version *version.Version
+		Reason  string
+	}{
+		{version.Must(version.NewVersion("2.43.1")), "regression bug of GIT_FLUSH"},
+	}
+	for _, bad := range badVersions {
+		if gitVer.Equal(bad.Version) {
+			return errors.New(bad.Reason)
+		}
 	}
 	return nil
 }
 
 func configSet(key, value string) error {
 	stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
-	if err != nil && !err.IsExitCode(1) {
+	if err != nil && !IsErrorExitCode(err, 1) {
 		return fmt.Errorf("failed to get git config %s, err: %w", key, err)
 	}
 
@@ -345,7 +364,7 @@ func configSetNonExist(key, value string) error {
 		// already exist
 		return nil
 	}
-	if err.IsExitCode(1) {
+	if IsErrorExitCode(err, 1) {
 		// not exist, set new config
 		_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil)
 		if err != nil {
@@ -363,7 +382,7 @@ func configAddNonExist(key, value string) error {
 		// already exist
 		return nil
 	}
-	if err.IsExitCode(1) {
+	if IsErrorExitCode(err, 1) {
 		// not exist, add new config
 		_, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil)
 		if err != nil {
@@ -384,7 +403,7 @@ func configUnsetAll(key, value string) error {
 		}
 		return nil
 	}
-	if err.IsExitCode(1) {
+	if IsErrorExitCode(err, 1) {
 		// not exist
 		return nil
 	}
diff --git a/modules/git/git_test.go b/modules/git/git_test.go
index 37ab669ea4..fc92bebe04 100644
--- a/modules/git/git_test.go
+++ b/modules/git/git_test.go
@@ -13,6 +13,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 
+	"github.com/hashicorp/go-version"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -93,3 +94,25 @@ func TestSyncConfig(t *testing.T) {
 	assert.True(t, gitConfigContains("[sync-test]"))
 	assert.True(t, gitConfigContains("cfg-key-a = CfgValA"))
 }
+
+func TestParseGitVersion(t *testing.T) {
+	v, err := parseGitVersionLine("git version 2.29.3")
+	assert.NoError(t, err)
+	assert.Equal(t, "2.29.3", v.String())
+
+	v, err = parseGitVersionLine("git version 2.29.3.windows.1")
+	assert.NoError(t, err)
+	assert.Equal(t, "2.29.3", v.String())
+
+	_, err = parseGitVersionLine("git version")
+	assert.Error(t, err)
+
+	_, err = parseGitVersionLine("git version windows")
+	assert.Error(t, err)
+}
+
+func TestCheckGitVersionCompatibility(t *testing.T) {
+	assert.NoError(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.0"))))
+	assert.ErrorContains(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.1"))), "regression bug of GIT_FLUSH")
+	assert.NoError(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.2"))))
+}
diff --git a/modules/git/grep.go b/modules/git/grep.go
new file mode 100644
index 0000000000..e7d238e586
--- /dev/null
+++ b/modules/git/grep.go
@@ -0,0 +1,130 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"slices"
+	"strconv"
+	"strings"
+
+	"code.gitea.io/gitea/modules/util"
+)
+
+type GrepResult struct {
+	Filename    string
+	LineNumbers []int
+	LineCodes   []string
+}
+
+type GrepOptions struct {
+	RefName           string
+	MaxResultLimit    int
+	ContextLineNumber int
+	IsFuzzy           bool
+	MaxLineLength     int // the maximum length of a line to parse, exceeding chars will be truncated
+}
+
+func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
+	stdoutReader, stdoutWriter, err := os.Pipe()
+	if err != nil {
+		return nil, fmt.Errorf("unable to create os pipe to grep: %w", err)
+	}
+	defer func() {
+		_ = stdoutReader.Close()
+		_ = stdoutWriter.Close()
+	}()
+
+	/*
+	 The output is like this ( "^@" means \x00):
+
+	 HEAD:.air.toml
+	 6^@bin = "gitea"
+
+	 HEAD:.changelog.yml
+	 2^@repo: go-gitea/gitea
+	*/
+	var results []*GrepResult
+	cmd := NewCommand(ctx, "grep", "--null", "--break", "--heading", "--fixed-strings", "--line-number", "--ignore-case", "--full-name")
+	cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber))
+	if opts.IsFuzzy {
+		words := strings.Fields(search)
+		for _, word := range words {
+			cmd.AddOptionValues("-e", strings.TrimLeft(word, "-"))
+		}
+	} else {
+		cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
+	}
+	cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD"))
+	opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
+	stderr := bytes.Buffer{}
+	err = cmd.Run(&RunOpts{
+		Dir:    repo.Path,
+		Stdout: stdoutWriter,
+		Stderr: &stderr,
+		PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
+			_ = stdoutWriter.Close()
+			defer stdoutReader.Close()
+
+			isInBlock := false
+			rd := bufio.NewReaderSize(stdoutReader, util.IfZero(opts.MaxLineLength, 16*1024))
+			var res *GrepResult
+			for {
+				lineBytes, isPrefix, err := rd.ReadLine()
+				if isPrefix {
+					lineBytes = slices.Clone(lineBytes)
+					for isPrefix && err == nil {
+						_, isPrefix, err = rd.ReadLine()
+					}
+				}
+				if len(lineBytes) == 0 && err != nil {
+					break
+				}
+				line := string(lineBytes) // the memory of lineBytes is mutable
+				if !isInBlock {
+					if _ /* ref */, filename, ok := strings.Cut(line, ":"); ok {
+						isInBlock = true
+						res = &GrepResult{Filename: filename}
+						results = append(results, res)
+					}
+					continue
+				}
+				if line == "" {
+					if len(results) >= opts.MaxResultLimit {
+						cancel()
+						break
+					}
+					isInBlock = false
+					continue
+				}
+				if line == "--" {
+					continue
+				}
+				if lineNum, lineCode, ok := strings.Cut(line, "\x00"); ok {
+					lineNumInt, _ := strconv.Atoi(lineNum)
+					res.LineNumbers = append(res.LineNumbers, lineNumInt)
+					res.LineCodes = append(res.LineCodes, lineCode)
+				}
+			}
+			return nil
+		},
+	})
+	// git grep exits by cancel (killed), usually it is caused by the limit of results
+	if IsErrorExitCode(err, -1) && stderr.Len() == 0 {
+		return results, nil
+	}
+	// git grep exits with 1 if no results are found
+	if IsErrorExitCode(err, 1) && stderr.Len() == 0 {
+		return nil, nil
+	}
+	if err != nil && !errors.Is(err, context.Canceled) {
+		return nil, fmt.Errorf("unable to run git grep: %w, stderr: %s", err, stderr.String())
+	}
+	return results, nil
+}
diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go
new file mode 100644
index 0000000000..7f4ded478f
--- /dev/null
+++ b/modules/git/grep_test.go
@@ -0,0 +1,61 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"context"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGrepSearch(t *testing.T) {
+	repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "language_stats_repo"))
+	assert.NoError(t, err)
+	defer repo.Close()
+
+	res, err := GrepSearch(context.Background(), repo, "void", GrepOptions{})
+	assert.NoError(t, err)
+	assert.Equal(t, []*GrepResult{
+		{
+			Filename:    "java-hello/main.java",
+			LineNumbers: []int{3},
+			LineCodes:   []string{" public static void main(String[] args)"},
+		},
+		{
+			Filename:    "main.vendor.java",
+			LineNumbers: []int{3},
+			LineCodes:   []string{" public static void main(String[] args)"},
+		},
+	}, res)
+
+	res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1})
+	assert.NoError(t, err)
+	assert.Equal(t, []*GrepResult{
+		{
+			Filename:    "java-hello/main.java",
+			LineNumbers: []int{3},
+			LineCodes:   []string{" public static void main(String[] args)"},
+		},
+	}, res)
+
+	res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1, MaxLineLength: 39})
+	assert.NoError(t, err)
+	assert.Equal(t, []*GrepResult{
+		{
+			Filename:    "java-hello/main.java",
+			LineNumbers: []int{3},
+			LineCodes:   []string{" public static void main(String[] arg"},
+		},
+	}, res)
+
+	res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{})
+	assert.NoError(t, err)
+	assert.Len(t, res, 0)
+
+	res, err = GrepSearch(context.Background(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{})
+	assert.Error(t, err)
+	assert.Len(t, res, 0)
+}
diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go
index 7c7baedd2f..5b62b90b27 100644
--- a/modules/git/last_commit_cache.go
+++ b/modules/git/last_commit_cache.go
@@ -4,12 +4,11 @@
 package git
 
 import (
+	"crypto/sha256"
 	"fmt"
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-
-	"github.com/minio/sha256-simd"
 )
 
 // Cache represents a caching interface
diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go
index 26a0d28098..9e345f3ee0 100644
--- a/modules/git/log_name_status.go
+++ b/modules/git/log_name_status.go
@@ -143,19 +143,19 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int
 	}
 
 	// Our "line" must look like: <commitid> SP (<parent> SP) * NUL
-	commitIds := string(g.next)
+	commitIDs := string(g.next)
 	if g.buffull {
 		more, err := g.rd.ReadString('\x00')
 		if err != nil {
 			return nil, err
 		}
-		commitIds += more
+		commitIDs += more
 	}
-	commitIds = commitIds[:len(commitIds)-1]
-	splitIds := strings.Split(commitIds, " ")
-	ret.CommitID = splitIds[0]
-	if len(splitIds) > 1 {
-		ret.ParentIDs = splitIds[1:]
+	commitIDs = commitIDs[:len(commitIDs)-1]
+	splitIDs := strings.Split(commitIDs, " ")
+	ret.CommitID = splitIDs[0]
+	if len(splitIDs) > 1 {
+		ret.ParentIDs = splitIDs[1:]
 	}
 
 	// now read the next "line"
diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go
index a725f4799d..4c65249089 100644
--- a/modules/git/pipeline/lfs_nogogit.go
+++ b/modules/git/pipeline/lfs_nogogit.go
@@ -169,6 +169,10 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
 				} else {
 					break commitReadingLoop
 				}
+			default:
+				if err := git.DiscardFull(batchReader, size+1); err != nil {
+					return nil, err
+				}
 			}
 		}
 	}
diff --git a/modules/git/repo.go b/modules/git/repo.go
index 028ca485f3..c5ba5117a7 100644
--- a/modules/git/repo.go
+++ b/modules/git/repo.go
@@ -74,7 +74,7 @@ func InitRepository(ctx context.Context, repoPath string, bare bool, objectForma
 	if !IsValidObjectFormat(objectFormatName) {
 		return fmt.Errorf("invalid object format: %s", objectFormatName)
 	}
-	if SupportHashSha256 {
+	if DefaultFeatures.SupportHashSha256 {
 		cmd.AddOptionValues("--object-format", objectFormatName)
 	}
 
@@ -244,7 +244,7 @@ func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error
 		return time.Time{}, err
 	}
 	commitTime := strings.TrimSpace(stdout)
-	return time.Parse(GitTimeLayout, commitTime)
+	return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime)
 }
 
 // DivergeObject represents commit count diverging commits
@@ -256,7 +256,7 @@ type DivergeObject struct {
 // GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch
 func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) {
 	cmd := NewCommand(ctx, "rev-list", "--count", "--left-right").
-		AddDynamicArguments(baseBranch + "..." + targetBranch)
+		AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--")
 	stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
 	if err != nil {
 		return do, err
diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go
index 2b34f117f7..84f85d1b1a 100644
--- a/modules/git/repo_attribute.go
+++ b/modules/git/repo_attribute.go
@@ -291,10 +291,17 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe
 	}
 
 	checker := &CheckAttributeReader{
-		Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
-		Repo:       repo,
-		IndexFile:  indexFilename,
-		WorkTree:   worktree,
+		Attributes: []string{
+			AttributeLinguistVendored,
+			AttributeLinguistGenerated,
+			AttributeLinguistDocumentation,
+			AttributeLinguistDetectable,
+			AttributeLinguistLanguage,
+			AttributeGitlabLanguage,
+		},
+		Repo:      repo,
+		IndexFile: indexFilename,
+		WorkTree:  worktree,
 	}
 	ctx, cancel := context.WithCancel(repo.Ctx)
 	if err := checker.Init(ctx); err != nil {
diff --git a/modules/git/repo_attribute_test.go b/modules/git/repo_attribute_test.go
index ed16dccbe4..0fcd94b4c7 100644
--- a/modules/git/repo_attribute_test.go
+++ b/modules/git/repo_attribute_test.go
@@ -24,7 +24,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
 	select {
 	case attr := <-wr.ReadAttribute():
 		assert.Equal(t, ".gitignore\"\n", attr.Filename)
-		assert.Equal(t, "linguist-vendored", attr.Attribute)
+		assert.Equal(t, AttributeLinguistVendored, attr.Attribute)
 		assert.Equal(t, "unspecified", attr.Value)
 	case <-time.After(100 * time.Millisecond):
 		assert.FailNow(t, "took too long to read an attribute from the list")
@@ -38,7 +38,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
 	select {
 	case attr := <-wr.ReadAttribute():
 		assert.Equal(t, ".gitignore\"\n", attr.Filename)
-		assert.Equal(t, "linguist-vendored", attr.Attribute)
+		assert.Equal(t, AttributeLinguistVendored, attr.Attribute)
 		assert.Equal(t, "unspecified", attr.Value)
 	case <-time.After(100 * time.Millisecond):
 		assert.FailNow(t, "took too long to read an attribute from the list")
@@ -77,21 +77,21 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, attributeTriple{
 		Filename:  "shouldbe.vendor",
-		Attribute: "linguist-vendored",
+		Attribute: AttributeLinguistVendored,
 		Value:     "set",
 	}, attr)
 	attr = <-wr.ReadAttribute()
 	assert.NoError(t, err)
 	assert.EqualValues(t, attributeTriple{
 		Filename:  "shouldbe.vendor",
-		Attribute: "linguist-generated",
+		Attribute: AttributeLinguistGenerated,
 		Value:     "unspecified",
 	}, attr)
 	attr = <-wr.ReadAttribute()
 	assert.NoError(t, err)
 	assert.EqualValues(t, attributeTriple{
 		Filename:  "shouldbe.vendor",
-		Attribute: "linguist-language",
+		Attribute: AttributeLinguistLanguage,
 		Value:     "unspecified",
 	}, attr)
 }
diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go
index 9270bb70f0..0cd07dcdc8 100644
--- a/modules/git/repo_base_gogit.go
+++ b/modules/git/repo_base_gogit.go
@@ -8,11 +8,11 @@ package git
 
 import (
 	"context"
-	"errors"
 	"path/filepath"
 
 	gitealog "code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 
 	"github.com/go-git/go-billy/v5"
 	"github.com/go-git/go-billy/v5/osfs"
@@ -52,7 +52,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
 	if err != nil {
 		return nil, err
 	} else if !isDir(repoPath) {
-		return nil, errors.New("no such file or directory")
+		return nil, util.NewNotExistErrorf("no such file or directory")
 	}
 
 	fs := osfs.New(repoPath)
@@ -88,16 +88,17 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
 }
 
 // Close this repository, in particular close the underlying gogitStorage if this is not nil
-func (repo *Repository) Close() (err error) {
+func (repo *Repository) Close() error {
 	if repo == nil || repo.gogitStorage == nil {
-		return
+		return nil
 	}
 	if err := repo.gogitStorage.Close(); err != nil {
 		gitealog.Error("Error closing storage: %v", err)
 	}
+	repo.gogitStorage = nil
 	repo.LastCommitCache = nil
 	repo.tagCache = nil
-	return
+	return nil
 }
 
 // GoGitRepo gets the go-git repo representation
diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go
index d5a350a926..5511526e78 100644
--- a/modules/git/repo_base_nogogit.go
+++ b/modules/git/repo_base_nogogit.go
@@ -9,10 +9,10 @@ package git
 import (
 	"bufio"
 	"context"
-	"errors"
 	"path/filepath"
 
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
 )
 
 func init() {
@@ -27,10 +27,12 @@ type Repository struct {
 
 	gpgSettings *GPGSettings
 
+	batchInUse  bool
 	batchCancel context.CancelFunc
 	batchReader *bufio.Reader
 	batchWriter WriteCloserError
 
+	checkInUse  bool
 	checkCancel context.CancelFunc
 	checkReader *bufio.Reader
 	checkWriter WriteCloserError
@@ -52,7 +54,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
 	if err != nil {
 		return nil, err
 	} else if !isDir(repoPath) {
-		return nil, errors.New("no such file or directory")
+		return nil, util.NewNotExistErrorf("no such file or directory")
 	}
 
 	// Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first!
@@ -69,34 +71,34 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
 	repo.batchWriter, repo.batchReader, repo.batchCancel = CatFileBatch(ctx, repoPath)
 	repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repoPath)
 
-	repo.objectFormat, err = repo.GetObjectFormat()
-	if err != nil {
-		return nil, err
-	}
-
 	return repo, nil
 }
 
 // CatFileBatch obtains a CatFileBatch for this repository
 func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) {
-	if repo.batchCancel == nil || repo.batchReader.Buffered() > 0 {
+	if repo.batchCancel == nil || repo.batchInUse {
 		log.Debug("Opening temporary cat file batch for: %s", repo.Path)
 		return CatFileBatch(ctx, repo.Path)
 	}
-	return repo.batchWriter, repo.batchReader, func() {}
+	repo.batchInUse = true
+	return repo.batchWriter, repo.batchReader, func() {
+		repo.batchInUse = false
+	}
 }
 
 // CatFileBatchCheck obtains a CatFileBatchCheck for this repository
 func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) {
-	if repo.checkCancel == nil || repo.checkReader.Buffered() > 0 {
-		log.Debug("Opening temporary cat file batch-check: %s", repo.Path)
+	if repo.checkCancel == nil || repo.checkInUse {
+		log.Debug("Opening temporary cat file batch-check for: %s", repo.Path)
 		return CatFileBatchCheck(ctx, repo.Path)
 	}
-	return repo.checkWriter, repo.checkReader, func() {}
+	repo.checkInUse = true
+	return repo.checkWriter, repo.checkReader, func() {
+		repo.checkInUse = false
+	}
 }
 
-// Close this repository, in particular close the underlying gogitStorage if this is not nil
-func (repo *Repository) Close() (err error) {
+func (repo *Repository) Close() error {
 	if repo == nil {
 		return nil
 	}
@@ -105,14 +107,16 @@ func (repo *Repository) Close() (err error) {
 		repo.batchReader = nil
 		repo.batchWriter = nil
 		repo.batchCancel = nil
+		repo.batchInUse = false
 	}
 	if repo.checkCancel != nil {
 		repo.checkCancel()
 		repo.checkCancel = nil
 		repo.checkReader = nil
 		repo.checkWriter = nil
+		repo.checkInUse = false
 	}
 	repo.LastCommitCache = nil
 	repo.tagCache = nil
-	return err
+	return nil
 }
diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go
index 4d5f06dca4..12f88feb9a 100644
--- a/modules/git/repo_branch.go
+++ b/modules/git/repo_branch.go
@@ -5,6 +5,7 @@
 package git
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"strings"
@@ -43,15 +44,8 @@ func (repo *Repository) GetHEADBranch() (*Branch, error) {
 	}, nil
 }
 
-// SetDefaultBranch sets default branch of repository.
-func (repo *Repository) SetDefaultBranch(name string) error {
-	_, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").AddDynamicArguments(BranchPrefix + name).RunStdString(&RunOpts{Dir: repo.Path})
-	return err
-}
-
-// GetDefaultBranch gets default branch of repository.
-func (repo *Repository) GetDefaultBranch() (string, error) {
-	stdout, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repo.Path})
+func GetDefaultBranch(ctx context.Context, repoPath string) (string, error) {
+	stdout, _, err := NewCommand(ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repoPath})
 	if err != nil {
 		return "", err
 	}
diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go
index 9c9ee7768f..44273d2253 100644
--- a/modules/git/repo_commit.go
+++ b/modules/git/repo_commit.go
@@ -246,7 +246,12 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions)
 		}
 	}()
 
-	len := repo.objectFormat.FullLength()
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
+
+	len := objectFormat.FullLength()
 	commits := []*Commit{}
 	shaline := make([]byte, len+1)
 	for {
diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go
index 4cab957564..84580be9a5 100644
--- a/modules/git/repo_commit_gogit.go
+++ b/modules/git/repo_commit_gogit.go
@@ -41,7 +41,10 @@ func (repo *Repository) RemoveReference(name string) error {
 
 // ConvertToHash returns a Hash object from a potential ID string
 func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
-	objectFormat := repo.objectFormat
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
 	if len(commitID) == hash.HexSize && objectFormat.IsValid(commitID) {
 		ID, err := NewIDFromString(commitID)
 		if err == nil {
diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go
index f0214e1ff8..ae4c21aaa3 100644
--- a/modules/git/repo_commit_nogogit.go
+++ b/modules/git/repo_commit_nogogit.go
@@ -121,8 +121,7 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID)
 		return commit, nil
 	default:
 		log.Debug("Unknown typ: %s", typ)
-		_, err = rd.Discard(int(size) + 1)
-		if err != nil {
+		if err := DiscardFull(rd, size+1); err != nil {
 			return nil, err
 		}
 		return nil, ErrNotExist{
@@ -133,8 +132,11 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID)
 
 // ConvertToGitID returns a GitHash object from a potential ID string
 func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
-	IDType := repo.objectFormat
-	if len(commitID) == IDType.FullLength() && IDType.IsValid(commitID) {
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
+	if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) {
 		ID, err := NewIDFromString(commitID)
 		if err == nil {
 			return ID, nil
@@ -143,7 +145,7 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
 
 	wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
 	defer cancel()
-	_, err := wr.Write([]byte(commitID + "\n"))
+	_, err = wr.Write([]byte(commitID + "\n"))
 	if err != nil {
 		return nil, err
 	}
diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go
index 0e9a0c70d7..b6e9d2b44a 100644
--- a/modules/git/repo_compare.go
+++ b/modules/git/repo_compare.go
@@ -283,8 +283,12 @@ func (repo *Repository) GetPatch(base, head string, w io.Writer) error {
 // If base is undefined empty SHA (zeros), it only returns the files changed in the head commit
 // If base is the SHA of an empty tree (EmptyTreeSHA), it returns the files changes from the initial commit to the head commit
 func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, error) {
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
 	cmd := NewCommand(repo.Ctx, "diff-tree", "--name-only", "--root", "--no-commit-id", "-r", "-z")
-	if base == repo.objectFormat.EmptyObjectID().String() {
+	if base == objectFormat.EmptyObjectID().String() {
 		cmd.AddDynamicArguments(head)
 	} else {
 		cmd.AddDynamicArguments(base, head)
diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go
index 526b213550..9983873186 100644
--- a/modules/git/repo_compare_test.go
+++ b/modules/git/repo_compare_test.go
@@ -126,17 +126,20 @@ func TestGetCommitFilesChanged(t *testing.T) {
 	assert.NoError(t, err)
 	defer repo.Close()
 
+	objectFormat, err := repo.GetObjectFormat()
+	assert.NoError(t, err)
+
 	testCases := []struct {
 		base, head string
 		files      []string
 	}{
 		{
-			repo.objectFormat.EmptyObjectID().String(),
+			objectFormat.EmptyObjectID().String(),
 			"95bb4d39648ee7e325106df01a621c530863a653",
 			[]string{"file1.txt"},
 		},
 		{
-			repo.objectFormat.EmptyObjectID().String(),
+			objectFormat.EmptyObjectID().String(),
 			"8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2",
 			[]string{"file2.txt"},
 		},
@@ -146,7 +149,7 @@ func TestGetCommitFilesChanged(t *testing.T) {
 			[]string{"file2.txt"},
 		},
 		{
-			repo.objectFormat.EmptyTree().String(),
+			objectFormat.EmptyTree().String(),
 			"8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2",
 			[]string{"file1.txt", "file2.txt"},
 		},
diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go
index 47705a92af..6aaab242c1 100644
--- a/modules/git/repo_index.go
+++ b/modules/git/repo_index.go
@@ -94,6 +94,10 @@ func (repo *Repository) LsFiles(filenames ...string) ([]string, error) {
 
 // RemoveFilesFromIndex removes given filenames from the index - it does not check whether they are present.
 func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error {
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return err
+	}
 	cmd := NewCommand(repo.Ctx, "update-index", "--remove", "-z", "--index-info")
 	stdout := new(bytes.Buffer)
 	stderr := new(bytes.Buffer)
@@ -101,7 +105,7 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error {
 	for _, file := range filenames {
 		if file != "" {
 			buffer.WriteString("0 ")
-			buffer.WriteString(repo.objectFormat.EmptyObjectID().String())
+			buffer.WriteString(objectFormat.EmptyObjectID().String())
 			buffer.WriteByte('\t')
 			buffer.WriteString(file)
 			buffer.WriteByte('\000')
diff --git a/modules/git/repo_language_stats.go b/modules/git/repo_language_stats.go
index c40d6937b5..8551ea9d24 100644
--- a/modules/git/repo_language_stats.go
+++ b/modules/git/repo_language_stats.go
@@ -6,6 +6,8 @@ package git
 import (
 	"strings"
 	"unicode"
+
+	"code.gitea.io/gitea/modules/optional"
 )
 
 const (
@@ -46,3 +48,20 @@ func mergeLanguageStats(stats map[string]int64) map[string]int64 {
 	}
 	return res
 }
+
+func TryReadLanguageAttribute(attrs map[string]string) optional.Option[string] {
+	language := AttributeToString(attrs, AttributeLinguistLanguage)
+	if language.Value() == "" {
+		language = AttributeToString(attrs, AttributeGitlabLanguage)
+		if language.Has() {
+			raw := language.Value()
+			// gitlab-language may have additional parameters after the language
+			// ignore them and just use the main language
+			// https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type
+			if idx := strings.IndexByte(raw, '?'); idx >= 0 {
+				language = optional.Some(raw[:idx])
+			}
+		}
+	}
+	return language
+}
diff --git a/modules/git/repo_language_stats_gogit.go b/modules/git/repo_language_stats_gogit.go
index 4c6fbd6c7e..a34c03c781 100644
--- a/modules/git/repo_language_stats_gogit.go
+++ b/modules/git/repo_language_stats_gogit.go
@@ -8,9 +8,9 @@ package git
 import (
 	"bytes"
 	"io"
-	"strings"
 
 	"code.gitea.io/gitea/modules/analyze"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/go-enry/go-enry/v2"
 	"github.com/go-git/go-git/v5"
@@ -57,25 +57,38 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 			return nil
 		}
 
-		notVendored := false
-		notGenerated := false
+		isVendored := optional.None[bool]()
+		isGenerated := optional.None[bool]()
+		isDocumentation := optional.None[bool]()
+		isDetectable := optional.None[bool]()
 
 		if checker != nil {
 			attrs, err := checker.CheckPath(f.Name)
 			if err == nil {
-				if vendored, has := attrs["linguist-vendored"]; has {
-					if vendored == "set" || vendored == "true" {
-						return nil
-					}
-					notVendored = vendored == "false"
+				isVendored = AttributeToBool(attrs, AttributeLinguistVendored)
+				if isVendored.ValueOrDefault(false) {
+					return nil
 				}
-				if generated, has := attrs["linguist-generated"]; has {
-					if generated == "set" || generated == "true" {
-						return nil
-					}
-					notGenerated = generated == "false"
+
+				isGenerated = AttributeToBool(attrs, AttributeLinguistGenerated)
+				if isGenerated.ValueOrDefault(false) {
+					return nil
 				}
-				if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
+
+				isDocumentation = AttributeToBool(attrs, AttributeLinguistDocumentation)
+				if isDocumentation.ValueOrDefault(false) {
+					return nil
+				}
+
+				isDetectable = AttributeToBool(attrs, AttributeLinguistDetectable)
+				if !isDetectable.ValueOrDefault(true) {
+					return nil
+				}
+
+				hasLanguage := TryReadLanguageAttribute(attrs)
+				if hasLanguage.Value() != "" {
+					language := hasLanguage.Value()
+
 					// group languages, such as Pug -> HTML; SCSS -> CSS
 					group := enry.GetLanguageGroup(language)
 					if len(group) != 0 {
@@ -85,28 +98,14 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 					// this language will always be added to the size
 					sizes[language] += f.Size
 					return nil
-				} else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
-					// strip off a ? if present
-					if idx := strings.IndexByte(language, '?'); idx >= 0 {
-						language = language[:idx]
-					}
-					if len(language) != 0 {
-						// group languages, such as Pug -> HTML; SCSS -> CSS
-						group := enry.GetLanguageGroup(language)
-						if len(group) != 0 {
-							language = group
-						}
-
-						// this language will always be added to the size
-						sizes[language] += f.Size
-						return nil
-					}
 				}
 			}
 		}
 
-		if (!notVendored && analyze.IsVendor(f.Name)) || enry.IsDotFile(f.Name) ||
-			enry.IsDocumentation(f.Name) || enry.IsConfiguration(f.Name) {
+		if (!isVendored.Has() && analyze.IsVendor(f.Name)) ||
+			enry.IsDotFile(f.Name) ||
+			(!isDocumentation.Has() && enry.IsDocumentation(f.Name)) ||
+			enry.IsConfiguration(f.Name) {
 			return nil
 		}
 
@@ -115,12 +114,10 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 		if f.Size <= bigFileSize {
 			content, _ = readFile(f, fileSizeLimit)
 		}
-		if !notGenerated && enry.IsGenerated(f.Name, content) {
+		if !isGenerated.Has() && enry.IsGenerated(f.Name, content) {
 			return nil
 		}
 
-		// TODO: Use .gitattributes file for linguist overrides
-
 		language := analyze.GetCodeLanguage(f.Name, content)
 		if language == enry.OtherLanguage || language == "" {
 			return nil
@@ -138,7 +135,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 			included = langtype == enry.Programming || langtype == enry.Markup
 			includedLanguage[language] = included
 		}
-		if included {
+		if included || isDetectable.ValueOrDefault(false) {
 			sizes[language] += f.Size
 		} else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) {
 			firstExcludedLanguage = language
diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go
index 1d94ad6c00..318fc091ce 100644
--- a/modules/git/repo_language_stats_nogogit.go
+++ b/modules/git/repo_language_stats_nogogit.go
@@ -6,14 +6,12 @@
 package git
 
 import (
-	"bufio"
 	"bytes"
 	"io"
-	"math"
-	"strings"
 
 	"code.gitea.io/gitea/modules/analyze"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/go-enry/go-enry/v2"
 )
@@ -90,25 +88,38 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 			continue
 		}
 
-		notVendored := false
-		notGenerated := false
+		isVendored := optional.None[bool]()
+		isGenerated := optional.None[bool]()
+		isDocumentation := optional.None[bool]()
+		isDetectable := optional.None[bool]()
 
 		if checker != nil {
 			attrs, err := checker.CheckPath(f.Name())
 			if err == nil {
-				if vendored, has := attrs["linguist-vendored"]; has {
-					if vendored == "set" || vendored == "true" {
-						continue
-					}
-					notVendored = vendored == "false"
+				isVendored = AttributeToBool(attrs, AttributeLinguistVendored)
+				if isVendored.ValueOrDefault(false) {
+					continue
 				}
-				if generated, has := attrs["linguist-generated"]; has {
-					if generated == "set" || generated == "true" {
-						continue
-					}
-					notGenerated = generated == "false"
+
+				isGenerated = AttributeToBool(attrs, AttributeLinguistGenerated)
+				if isGenerated.ValueOrDefault(false) {
+					continue
 				}
-				if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
+
+				isDocumentation = AttributeToBool(attrs, AttributeLinguistDocumentation)
+				if isDocumentation.ValueOrDefault(false) {
+					continue
+				}
+
+				isDetectable = AttributeToBool(attrs, AttributeLinguistDetectable)
+				if !isDetectable.ValueOrDefault(true) {
+					continue
+				}
+
+				hasLanguage := TryReadLanguageAttribute(attrs)
+				if hasLanguage.Value() != "" {
+					language := hasLanguage.Value()
+
 					// group languages, such as Pug -> HTML; SCSS -> CSS
 					group := enry.GetLanguageGroup(language)
 					if len(group) != 0 {
@@ -118,29 +129,14 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 					// this language will always be added to the size
 					sizes[language] += f.Size()
 					continue
-				} else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
-					// strip off a ? if present
-					if idx := strings.IndexByte(language, '?'); idx >= 0 {
-						language = language[:idx]
-					}
-					if len(language) != 0 {
-						// group languages, such as Pug -> HTML; SCSS -> CSS
-						group := enry.GetLanguageGroup(language)
-						if len(group) != 0 {
-							language = group
-						}
-
-						// this language will always be added to the size
-						sizes[language] += f.Size()
-						continue
-					}
 				}
-
 			}
 		}
 
-		if (!notVendored && analyze.IsVendor(f.Name())) || enry.IsDotFile(f.Name()) ||
-			enry.IsDocumentation(f.Name()) || enry.IsConfiguration(f.Name()) {
+		if (!isVendored.Has() && analyze.IsVendor(f.Name())) ||
+			enry.IsDotFile(f.Name()) ||
+			(!isDocumentation.Has() && enry.IsDocumentation(f.Name())) ||
+			enry.IsConfiguration(f.Name()) {
 			continue
 		}
 
@@ -168,12 +164,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 				return nil, err
 			}
 			content = contentBuf.Bytes()
-			err = discardFull(batchReader, discard)
-			if err != nil {
+			if err := DiscardFull(batchReader, discard); err != nil {
 				return nil, err
 			}
 		}
-		if !notGenerated && enry.IsGenerated(f.Name(), content) {
+		if !isGenerated.Has() && enry.IsGenerated(f.Name(), content) {
 			continue
 		}
 
@@ -196,13 +191,12 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 			included = langType == enry.Programming || langType == enry.Markup
 			includedLanguage[language] = included
 		}
-		if included {
+		if included || isDetectable.ValueOrDefault(false) {
 			sizes[language] += f.Size()
 		} else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) {
 			firstExcludedLanguage = language
 			firstExcludedLanguageSize += f.Size()
 		}
-		continue
 	}
 
 	// If there are no included languages add the first excluded language
@@ -212,21 +206,3 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 
 	return mergeLanguageStats(sizes), nil
 }
-
-func discardFull(rd *bufio.Reader, discard int64) error {
-	if discard > math.MaxInt32 {
-		n, err := rd.Discard(math.MaxInt32)
-		discard -= int64(n)
-		if err != nil {
-			return err
-		}
-	}
-	for discard > 0 {
-		n, err := rd.Discard(int(discard))
-		discard -= int64(n)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go
index 41f94e24f9..83220104bd 100644
--- a/modules/git/repo_stats.go
+++ b/modules/git/repo_stats.go
@@ -124,6 +124,10 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
 					}
 				}
 			}
+			if err = scanner.Err(); err != nil {
+				_ = stdoutReader.Close()
+				return fmt.Errorf("GetCodeActivityStats scan: %w", err)
+			}
 			a := make([]*CodeActivityAuthor, 0, len(authors))
 			for _, v := range authors {
 				a = append(a, v)
diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go
index 0262351a9a..818ac8c95c 100644
--- a/modules/git/repo_tag.go
+++ b/modules/git/repo_tag.go
@@ -135,7 +135,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) {
 			break
 		}
 
-		tag, err := parseTagRef(repo.objectFormat, ref)
+		tag, err := parseTagRef(ref)
 		if err != nil {
 			return nil, 0, fmt.Errorf("GetTagInfos: parse tag: %w", err)
 		}
@@ -155,7 +155,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) {
 }
 
 // parseTagRef parses a tag from a 'git for-each-ref'-produced reference.
-func parseTagRef(objectFormat ObjectFormat, ref map[string]string) (tag *Tag, err error) {
+func parseTagRef(ref map[string]string) (tag *Tag, err error) {
 	tag = &Tag{
 		Type: ref["objecttype"],
 		Name: ref["refname:lstrip=2"],
@@ -177,23 +177,17 @@ func parseTagRef(objectFormat ObjectFormat, ref map[string]string) (tag *Tag, er
 		}
 	}
 
-	tag.Tagger, err = newSignatureFromCommitline([]byte(ref["creator"]))
-	if err != nil {
-		return nil, fmt.Errorf("parse tagger: %w", err)
-	}
-
+	tag.Tagger = parseSignatureFromCommitLine(ref["creator"])
 	tag.Message = ref["contents"]
-	// strip PGP signature if present in contents field
-	pgpStart := strings.Index(tag.Message, beginpgp)
-	if pgpStart >= 0 {
-		tag.Message = tag.Message[0:pgpStart]
-	}
+
+	// strip any signature if present in contents field
+	_, tag.Message, _ = parsePayloadSignature(util.UnsafeStringToBytes(tag.Message), 0)
 
 	// annotated tag with GPG signature
 	if tag.Type == "tag" && ref["contents:signature"] != "" {
 		payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n",
 			tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message))
-		tag.Signature = &CommitGPGSignature{
+		tag.Signature = &CommitSignature{
 			Signature: ref["contents:signature"],
 			Payload:   payload,
 		}
diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go
index 5d98fadd54..cbab39f8c5 100644
--- a/modules/git/repo_tag_nogogit.go
+++ b/modules/git/repo_tag_nogogit.go
@@ -103,6 +103,9 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) {
 		return nil, err
 	}
 	if typ != "tag" {
+		if err := DiscardFull(rd, size+1); err != nil {
+			return nil, err
+		}
 		return nil, ErrNotExist{ID: tagID.String()}
 	}
 
diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go
index da7b1455a8..0117cb902d 100644
--- a/modules/git/repo_tag_test.go
+++ b/modules/git/repo_tag_test.go
@@ -194,7 +194,6 @@ func TestRepository_GetAnnotatedTag(t *testing.T) {
 }
 
 func TestRepository_parseTagRef(t *testing.T) {
-	sha1 := Sha1ObjectFormat
 	tests := []struct {
 		name string
 
@@ -227,7 +226,7 @@ func TestRepository_parseTagRef(t *testing.T) {
 				ID:        MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"),
 				Object:    MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"),
 				Type:      "commit",
-				Tagger:    parseAuthorLine(t, "Foo Bar <foo@bar.com> 1565789218 +0300"),
+				Tagger:    parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
 				Message:   "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n",
 				Signature: nil,
 			},
@@ -256,7 +255,7 @@ func TestRepository_parseTagRef(t *testing.T) {
 				ID:        MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"),
 				Object:    MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"),
 				Type:      "tag",
-				Tagger:    parseAuthorLine(t, "Foo Bar <foo@bar.com> 1565789218 +0300"),
+				Tagger:    parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
 				Message:   "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n",
 				Signature: nil,
 			},
@@ -314,9 +313,9 @@ qbHDASXl
 				ID:      MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"),
 				Object:  MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"),
 				Type:    "tag",
-				Tagger:  parseAuthorLine(t, "Foo Bar <foo@bar.com> 1565789218 +0300"),
+				Tagger:  parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
 				Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md",
-				Signature: &CommitGPGSignature{
+				Signature: &CommitSignature{
 					Signature: `-----BEGIN PGP SIGNATURE-----
 
 aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3
@@ -351,7 +350,7 @@ Add changelog of v1.9.1 (#7859)
 	for _, test := range tests {
 		tc := test // don't close over loop variable
 		t.Run(tc.name, func(t *testing.T) {
-			got, err := parseTagRef(sha1, tc.givenRef)
+			got, err := parseTagRef(tc.givenRef)
 
 			if tc.wantErr {
 				require.Error(t, err)
@@ -363,14 +362,3 @@ Add changelog of v1.9.1 (#7859)
 		})
 	}
 }
-
-func parseAuthorLine(t *testing.T, committer string) *Signature {
-	t.Helper()
-
-	sig, err := newSignatureFromCommitline([]byte(committer))
-	if err != nil {
-		t.Fatalf("parse author line '%s': %v", committer, err)
-	}
-
-	return sig
-}
diff --git a/modules/git/repo_tree_gogit.go b/modules/git/repo_tree_gogit.go
index 6391959e6a..dc97ce1344 100644
--- a/modules/git/repo_tree_gogit.go
+++ b/modules/git/repo_tree_gogit.go
@@ -21,7 +21,12 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
 
 // GetTree find the tree object in the repository.
 func (repo *Repository) GetTree(idStr string) (*Tree, error) {
-	if len(idStr) != repo.objectFormat.FullLength() {
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(idStr) != objectFormat.FullLength() {
 		res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(&RunOpts{Dir: repo.Path})
 		if err != nil {
 			return nil, err
diff --git a/modules/git/repo_tree_nogogit.go b/modules/git/repo_tree_nogogit.go
index 20c92a79ed..e82012de6f 100644
--- a/modules/git/repo_tree_nogogit.go
+++ b/modules/git/repo_tree_nogogit.go
@@ -51,13 +51,20 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
 	case "tree":
 		tree := NewTree(repo, id)
 		tree.ResolvedID = id
-		tree.entries, err = catBatchParseTreeEntries(repo.objectFormat, tree, rd, size)
+		objectFormat, err := repo.GetObjectFormat()
+		if err != nil {
+			return nil, err
+		}
+		tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, size)
 		if err != nil {
 			return nil, err
 		}
 		tree.entriesParsed = true
 		return tree, nil
 	default:
+		if err := DiscardFull(rd, size+1); err != nil {
+			return nil, err
+		}
 		return nil, ErrNotExist{
 			ID: id.String(),
 		}
@@ -66,7 +73,11 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
 
 // GetTree find the tree object in the repository.
 func (repo *Repository) GetTree(idStr string) (*Tree, error) {
-	if len(idStr) != repo.objectFormat.FullLength() {
+	objectFormat, err := repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
+	if len(idStr) != objectFormat.FullLength() {
 		res, err := repo.GetRefCommitID(idStr)
 		if err != nil {
 			return nil, err
diff --git a/modules/git/signature.go b/modules/git/signature.go
index b5b17f23b0..f50a097758 100644
--- a/modules/git/signature.go
+++ b/modules/git/signature.go
@@ -4,7 +4,46 @@
 
 package git
 
-const (
-	// GitTimeLayout is the (default) time layout used by git.
-	GitTimeLayout = "Mon Jan _2 15:04:05 2006 -0700"
+import (
+	"strconv"
+	"strings"
+	"time"
+
+	"code.gitea.io/gitea/modules/log"
 )
+
+// Helper to get a signature from the commit line, which looks like:
+//
+//	full name <user@example.com> 1378823654 +0200
+//
+// Haven't found the official reference for the standard format yet.
+// This function never fails, if the "line" can't be parsed, it returns a default Signature with "zero" time.
+func parseSignatureFromCommitLine(line string) *Signature {
+	sig := &Signature{}
+	s1, sx, ok1 := strings.Cut(line, " <")
+	s2, s3, ok2 := strings.Cut(sx, "> ")
+	if !ok1 || !ok2 {
+		sig.Name = line
+		return sig
+	}
+	sig.Name, sig.Email = s1, s2
+
+	if strings.Count(s3, " ") == 1 {
+		ts, tz, _ := strings.Cut(s3, " ")
+		seconds, _ := strconv.ParseInt(ts, 10, 64)
+		if tzTime, err := time.Parse("-0700", tz); err == nil {
+			sig.When = time.Unix(seconds, 0).In(tzTime.Location())
+		}
+	} else {
+		// the old gitea code tried to parse the date in a few different formats, but it's not clear why.
+		// according to public document, only the standard format "timestamp timezone" could be found, so drop other formats.
+		log.Error("suspicious commit line format: %q", line)
+		for _, fmt := range []string{ /*"Mon Jan _2 15:04:05 2006 -0700"*/ } {
+			if t, err := time.Parse(fmt, s3); err == nil {
+				sig.When = t
+				break
+			}
+		}
+	}
+	return sig
+}
diff --git a/modules/git/signature_gogit.go b/modules/git/signature_gogit.go
index c984ad6e20..1fc6aabceb 100644
--- a/modules/git/signature_gogit.go
+++ b/modules/git/signature_gogit.go
@@ -7,52 +7,8 @@
 package git
 
 import (
-	"bytes"
-	"strconv"
-	"strings"
-	"time"
-
 	"github.com/go-git/go-git/v5/plumbing/object"
 )
 
 // Signature represents the Author or Committer information.
 type Signature = object.Signature
-
-// Helper to get a signature from the commit line, which looks like these:
-//
-//	author Patrick Gundlach <gundlach@speedata.de> 1378823654 +0200
-//	author Patrick Gundlach <gundlach@speedata.de> Thu, 07 Apr 2005 22:13:13 +0200
-//
-// but without the "author " at the beginning (this method should)
-// be used for author and committer.
-//
-// FIXME: include timezone for timestamp!
-func newSignatureFromCommitline(line []byte) (_ *Signature, err error) {
-	sig := new(Signature)
-	emailStart := bytes.IndexByte(line, '<')
-	if emailStart > 0 { // Empty name has already occurred, even if it shouldn't
-		sig.Name = strings.TrimSpace(string(line[:emailStart-1]))
-	}
-	emailEnd := bytes.IndexByte(line, '>')
-	sig.Email = string(line[emailStart+1 : emailEnd])
-
-	// Check date format.
-	if len(line) > emailEnd+2 {
-		firstChar := line[emailEnd+2]
-		if firstChar >= 48 && firstChar <= 57 {
-			timestop := bytes.IndexByte(line[emailEnd+2:], ' ')
-			timestring := string(line[emailEnd+2 : emailEnd+2+timestop])
-			seconds, _ := strconv.ParseInt(timestring, 10, 64)
-			sig.When = time.Unix(seconds, 0)
-		} else {
-			sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:]))
-			if err != nil {
-				return nil, err
-			}
-		}
-	} else {
-		// Fall back to unix 0 time
-		sig.When = time.Unix(0, 0)
-	}
-	return sig, nil
-}
diff --git a/modules/git/signature_nogogit.go b/modules/git/signature_nogogit.go
index 25277f99d5..0d19c0abdc 100644
--- a/modules/git/signature_nogogit.go
+++ b/modules/git/signature_nogogit.go
@@ -7,21 +7,17 @@
 package git
 
 import (
-	"bytes"
 	"fmt"
-	"strconv"
-	"strings"
 	"time"
+
+	"code.gitea.io/gitea/modules/util"
 )
 
-// Signature represents the Author or Committer information.
+// Signature represents the Author, Committer or Tagger information.
 type Signature struct {
-	// Name represents a person name. It is an arbitrary string.
-	Name string
-	// Email is an email, but it cannot be assumed to be well-formed.
-	Email string
-	// When is the timestamp of the signature.
-	When time.Time
+	Name  string    // the committer name, it can be anything
+	Email string    // the committer email, it can be anything
+	When  time.Time // the timestamp of the signature
 }
 
 func (s *Signature) String() string {
@@ -30,71 +26,5 @@ func (s *Signature) String() string {
 
 // Decode decodes a byte array representing a signature to signature
 func (s *Signature) Decode(b []byte) {
-	sig, _ := newSignatureFromCommitline(b)
-	s.Email = sig.Email
-	s.Name = sig.Name
-	s.When = sig.When
-}
-
-// Helper to get a signature from the commit line, which looks like these:
-//
-//	author Patrick Gundlach <gundlach@speedata.de> 1378823654 +0200
-//	author Patrick Gundlach <gundlach@speedata.de> Thu, 07 Apr 2005 22:13:13 +0200
-//
-// but without the "author " at the beginning (this method should)
-// be used for author and committer.
-// FIXME: there are a lot of "return sig, err" (but the err is also nil), that's the old behavior, to avoid breaking
-func newSignatureFromCommitline(line []byte) (sig *Signature, err error) {
-	sig = new(Signature)
-	emailStart := bytes.LastIndexByte(line, '<')
-	emailEnd := bytes.LastIndexByte(line, '>')
-	if emailStart == -1 || emailEnd == -1 || emailEnd < emailStart {
-		return sig, err
-	}
-
-	if emailStart > 0 { // Empty name has already occurred, even if it shouldn't
-		sig.Name = strings.TrimSpace(string(line[:emailStart-1]))
-	}
-	sig.Email = string(line[emailStart+1 : emailEnd])
-
-	hasTime := emailEnd+2 < len(line)
-	if !hasTime {
-		return sig, err
-	}
-
-	// Check date format.
-	firstChar := line[emailEnd+2]
-	if firstChar >= 48 && firstChar <= 57 {
-		idx := bytes.IndexByte(line[emailEnd+2:], ' ')
-		if idx < 0 {
-			return sig, err
-		}
-
-		timestring := string(line[emailEnd+2 : emailEnd+2+idx])
-		seconds, _ := strconv.ParseInt(timestring, 10, 64)
-		sig.When = time.Unix(seconds, 0)
-
-		idx += emailEnd + 3
-		if idx >= len(line) || idx+5 > len(line) {
-			return sig, err
-		}
-
-		timezone := string(line[idx : idx+5])
-		tzhours, err1 := strconv.ParseInt(timezone[0:3], 10, 64)
-		tzmins, err2 := strconv.ParseInt(timezone[3:], 10, 64)
-		if err1 != nil || err2 != nil {
-			return sig, err
-		}
-		if tzhours < 0 {
-			tzmins *= -1
-		}
-		tz := time.FixedZone("", int(tzhours*60*60+tzmins*60))
-		sig.When = sig.When.In(tz)
-	} else {
-		sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:]))
-		if err != nil {
-			return sig, err
-		}
-	}
-	return sig, err
+	*s = *parseSignatureFromCommitLine(util.UnsafeBytesToString(b))
 }
diff --git a/modules/git/signature_test.go b/modules/git/signature_test.go
new file mode 100644
index 0000000000..92681feea9
--- /dev/null
+++ b/modules/git/signature_test.go
@@ -0,0 +1,47 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseSignatureFromCommitLine(t *testing.T) {
+	tests := []struct {
+		line string
+		want *Signature
+	}{
+		{
+			line: "a b <c@d.com> 12345 +0100",
+			want: &Signature{
+				Name:  "a b",
+				Email: "c@d.com",
+				When:  time.Unix(12345, 0).In(time.FixedZone("", 3600)),
+			},
+		},
+		{
+			line: "bad line",
+			want: &Signature{Name: "bad line"},
+		},
+		{
+			line: "bad < line",
+			want: &Signature{Name: "bad < line"},
+		},
+		{
+			line: "bad > line",
+			want: &Signature{Name: "bad > line"},
+		},
+		{
+			line: "bad-line <name@example.com>",
+			want: &Signature{Name: "bad-line <name@example.com>"},
+		},
+	}
+	for _, test := range tests {
+		got := parseSignatureFromCommitLine(test.line)
+		assert.EqualValues(t, test.want, got)
+	}
+}
diff --git a/modules/git/tag.go b/modules/git/tag.go
index 01a8d6f6a5..f7666aa89b 100644
--- a/modules/git/tag.go
+++ b/modules/git/tag.go
@@ -6,12 +6,8 @@ package git
 import (
 	"bytes"
 	"sort"
-	"strings"
-)
 
-const (
-	beginpgp = "\n-----BEGIN PGP SIGNATURE-----\n"
-	endpgp   = "\n-----END PGP SIGNATURE-----"
+	"code.gitea.io/gitea/modules/util"
 )
 
 // Tag represents a Git tag.
@@ -22,7 +18,7 @@ type Tag struct {
 	Type      string
 	Tagger    *Signature
 	Message   string
-	Signature *CommitGPGSignature
+	Signature *CommitSignature
 }
 
 // Commit return the commit of the tag reference
@@ -30,6 +26,36 @@ func (tag *Tag) Commit(gitRepo *Repository) (*Commit, error) {
 	return gitRepo.getCommit(tag.Object)
 }
 
+func parsePayloadSignature(data []byte, messageStart int) (payload, msg, sign string) {
+	pos := messageStart
+	signStart, signEnd := -1, -1
+	for {
+		eol := bytes.IndexByte(data[pos:], '\n')
+		if eol < 0 {
+			break
+		}
+		line := data[pos : pos+eol]
+		signType, hasPrefix := bytes.CutPrefix(line, []byte("-----BEGIN "))
+		signType, hasSuffix := bytes.CutSuffix(signType, []byte(" SIGNATURE-----"))
+		if hasPrefix && hasSuffix {
+			signEndBytes := append([]byte("\n-----END "), signType...)
+			signEndBytes = append(signEndBytes, []byte(" SIGNATURE-----")...)
+			signEnd = bytes.Index(data[pos:], signEndBytes)
+			if signEnd != -1 {
+				signStart = pos
+				signEnd = pos + signEnd + len(signEndBytes)
+			}
+		}
+		pos += eol + 1
+	}
+
+	if signStart != -1 && signEnd != -1 {
+		msgEnd := max(messageStart, signStart-1)
+		return string(data[:msgEnd]), string(data[messageStart:msgEnd]), string(data[signStart:signEnd])
+	}
+	return string(data), string(data[messageStart:]), ""
+}
+
 // Parse commit information from the (uncompressed) raw
 // data from the commit object.
 // \n\n separate headers from message
@@ -38,51 +64,37 @@ func parseTagData(objectFormat ObjectFormat, data []byte) (*Tag, error) {
 	tag.ID = objectFormat.EmptyObjectID()
 	tag.Object = objectFormat.EmptyObjectID()
 	tag.Tagger = &Signature{}
-	// we now have the contents of the commit object. Let's investigate...
-	nextline := 0
-l:
+
+	pos := 0
 	for {
-		eol := bytes.IndexByte(data[nextline:], '\n')
-		switch {
-		case eol > 0:
-			line := data[nextline : nextline+eol]
-			spacepos := bytes.IndexByte(line, ' ')
-			reftype := line[:spacepos]
-			switch string(reftype) {
-			case "object":
-				id, err := NewIDFromString(string(line[spacepos+1:]))
-				if err != nil {
-					return nil, err
-				}
-				tag.Object = id
-			case "type":
-				// A commit can have one or more parents
-				tag.Type = string(line[spacepos+1:])
-			case "tagger":
-				sig, err := newSignatureFromCommitline(line[spacepos+1:])
-				if err != nil {
-					return nil, err
-				}
-				tag.Tagger = sig
-			}
-			nextline += eol + 1
-		case eol == 0:
-			tag.Message = string(data[nextline+1:])
-			break l
-		default:
-			break l
+		eol := bytes.IndexByte(data[pos:], '\n')
+		if eol == -1 {
+			break // shouldn't happen, but could just tolerate it
 		}
+		if eol == 0 {
+			pos++
+			break // end of headers
+		}
+		line := data[pos : pos+eol]
+		key, val, _ := bytes.Cut(line, []byte(" "))
+		switch string(key) {
+		case "object":
+			id, err := NewIDFromString(string(val))
+			if err != nil {
+				return nil, err
+			}
+			tag.Object = id
+		case "type":
+			tag.Type = string(val) // A commit can have one or more parents
+		case "tagger":
+			tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(val))
+		}
+		pos += eol + 1
 	}
-	idx := strings.LastIndex(tag.Message, beginpgp)
-	if idx > 0 {
-		endSigIdx := strings.Index(tag.Message[idx:], endpgp)
-		if endSigIdx > 0 {
-			tag.Signature = &CommitGPGSignature{
-				Signature: tag.Message[idx+1 : idx+endSigIdx+len(endpgp)],
-				Payload:   string(data[:bytes.LastIndex(data, []byte(beginpgp))+1]),
-			}
-			tag.Message = tag.Message[:idx+1]
-		}
+	payload, msg, sign := parsePayloadSignature(data, pos)
+	tag.Message = msg
+	if len(sign) > 0 {
+		tag.Signature = &CommitSignature{Signature: sign, Payload: payload}
 	}
 	return tag, nil
 }
diff --git a/modules/git/tag_test.go b/modules/git/tag_test.go
index f980b0c560..ba02c28946 100644
--- a/modules/git/tag_test.go
+++ b/modules/git/tag_test.go
@@ -12,24 +12,28 @@ import (
 
 func Test_parseTagData(t *testing.T) {
 	testData := []struct {
-		data []byte
-		tag  Tag
+		data     string
+		expected Tag
 	}{
-		{data: []byte(`object 3b114ab800c6432ad42387ccf6bc8d4388a2885a
+		{
+			data: `object 3b114ab800c6432ad42387ccf6bc8d4388a2885a
 type commit
 tag 1.22.0
 tagger Lucas Michot <lucas@semalead.com> 1484491741 +0100
 
-`), tag: Tag{
-			Name:      "",
-			ID:        Sha1ObjectFormat.EmptyObjectID(),
-			Object:    &Sha1Hash{0x3b, 0x11, 0x4a, 0xb8, 0x0, 0xc6, 0x43, 0x2a, 0xd4, 0x23, 0x87, 0xcc, 0xf6, 0xbc, 0x8d, 0x43, 0x88, 0xa2, 0x88, 0x5a},
-			Type:      "commit",
-			Tagger:    &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0)},
-			Message:   "",
-			Signature: nil,
-		}},
-		{data: []byte(`object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc
+`,
+			expected: Tag{
+				Name:      "",
+				ID:        Sha1ObjectFormat.EmptyObjectID(),
+				Object:    MustIDFromString("3b114ab800c6432ad42387ccf6bc8d4388a2885a"),
+				Type:      "commit",
+				Tagger:    &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
+				Message:   "",
+				Signature: nil,
+			},
+		},
+		{
+			data: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc
 type commit
 tag 1.22.1
 tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100
@@ -37,37 +41,57 @@ tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100
 test message
 o
 
-ono`), tag: Tag{
-			Name:      "",
-			ID:        Sha1ObjectFormat.EmptyObjectID(),
-			Object:    &Sha1Hash{0x7c, 0xdf, 0x42, 0xc0, 0xb1, 0xcc, 0x76, 0x3a, 0xb7, 0xe4, 0xc3, 0x3c, 0x47, 0xa2, 0x4e, 0x27, 0xc6, 0x6b, 0xfc, 0xcc},
-			Type:      "commit",
-			Tagger:    &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0)},
-			Message:   "test message\no\n\nono",
-			Signature: nil,
-		}},
+ono`,
+			expected: Tag{
+				Name:      "",
+				ID:        Sha1ObjectFormat.EmptyObjectID(),
+				Object:    MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc"),
+				Type:      "commit",
+				Tagger:    &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0).In(time.FixedZone("", 3600))},
+				Message:   "test message\no\n\nono",
+				Signature: nil,
+			},
+		},
+		{
+			data: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa
+type commit
+tag v0
+tagger dummy user <dummy-email@example.com> 1484491741 +0100
+
+dummy message
+-----BEGIN SSH SIGNATURE-----
+dummy signature
+-----END SSH SIGNATURE-----
+`,
+			expected: Tag{
+				Name:    "",
+				ID:      Sha1ObjectFormat.EmptyObjectID(),
+				Object:  MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa"),
+				Type:    "commit",
+				Tagger:  &Signature{Name: "dummy user", Email: "dummy-email@example.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
+				Message: "dummy message",
+				Signature: &CommitSignature{
+					Signature: `-----BEGIN SSH SIGNATURE-----
+dummy signature
+-----END SSH SIGNATURE-----`,
+					Payload: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa
+type commit
+tag v0
+tagger dummy user <dummy-email@example.com> 1484491741 +0100
+
+dummy message`,
+				},
+			},
+		},
 	}
 
 	for _, test := range testData {
-		tag, err := parseTagData(Sha1ObjectFormat, test.data)
+		tag, err := parseTagData(Sha1ObjectFormat, []byte(test.data))
 		assert.NoError(t, err)
-		assert.EqualValues(t, test.tag.ID, tag.ID)
-		assert.EqualValues(t, test.tag.Object, tag.Object)
-		assert.EqualValues(t, test.tag.Name, tag.Name)
-		assert.EqualValues(t, test.tag.Message, tag.Message)
-		assert.EqualValues(t, test.tag.Type, tag.Type)
-		if test.tag.Signature != nil && assert.NotNil(t, tag.Signature) {
-			assert.EqualValues(t, test.tag.Signature.Signature, tag.Signature.Signature)
-			assert.EqualValues(t, test.tag.Signature.Payload, tag.Signature.Payload)
-		} else {
-			assert.Nil(t, tag.Signature)
-		}
-		if test.tag.Tagger != nil && assert.NotNil(t, tag.Tagger) {
-			assert.EqualValues(t, test.tag.Tagger.Name, tag.Tagger.Name)
-			assert.EqualValues(t, test.tag.Tagger.Email, tag.Tagger.Email)
-			assert.EqualValues(t, test.tag.Tagger.When.Unix(), tag.Tagger.When.Unix())
-		} else {
-			assert.Nil(t, tag.Tagger)
-		}
+		assert.Equal(t, test.expected, *tag)
 	}
+
+	tag, err := parseTagData(Sha1ObjectFormat, []byte("type commit\n\nfoo\n-----BEGIN SSH SIGNATURE-----\ncorrupted..."))
+	assert.NoError(t, err)
+	assert.Equal(t, "foo\n-----BEGIN SSH SIGNATURE-----\ncorrupted...", tag.Message)
 }
diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go
index 89d3aebbc0..a591485082 100644
--- a/modules/git/tree_nogogit.go
+++ b/modules/git/tree_nogogit.go
@@ -7,7 +7,6 @@ package git
 
 import (
 	"io"
-	"math"
 	"strings"
 )
 
@@ -63,19 +62,8 @@ func (t *Tree) ListEntries() (Entries, error) {
 		}
 
 		// Not a tree just use ls-tree instead
-		for sz > math.MaxInt32 {
-			discarded, err := rd.Discard(math.MaxInt32)
-			sz -= int64(discarded)
-			if err != nil {
-				return nil, err
-			}
-		}
-		for sz > 0 {
-			discarded, err := rd.Discard(int(sz))
-			sz -= int64(discarded)
-			if err != nil {
-				return nil, err
-			}
+		if err := DiscardFull(rd, sz+1); err != nil {
+			return nil, err
 		}
 	}
 
@@ -89,8 +77,11 @@ func (t *Tree) ListEntries() (Entries, error) {
 		return nil, runErr
 	}
 
-	var err error
-	t.entries, err = parseTreeEntries(t.repo.objectFormat, stdout, t)
+	objectFormat, err := t.repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
+	t.entries, err = parseTreeEntries(objectFormat, stdout, t)
 	if err == nil {
 		t.entriesParsed = true
 	}
@@ -113,8 +104,11 @@ func (t *Tree) listEntriesRecursive(extraArgs TrustedCmdArgs) (Entries, error) {
 		return nil, runErr
 	}
 
-	var err error
-	t.entriesRecursive, err = parseTreeEntries(t.repo.objectFormat, stdout, t)
+	objectFormat, err := t.repo.GetObjectFormat()
+	if err != nil {
+		return nil, err
+	}
+	t.entriesRecursive, err = parseTreeEntries(objectFormat, stdout, t)
 	if err == nil {
 		t.entriesRecursiveParsed = true
 	}
diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go
new file mode 100644
index 0000000000..6d2b5c84d5
--- /dev/null
+++ b/modules/git/tree_test.go
@@ -0,0 +1,27 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSubTree_Issue29101(t *testing.T) {
+	repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare"))
+	assert.NoError(t, err)
+	defer repo.Close()
+
+	commit, err := repo.GetCommit("ce064814f4a0d337b333e646ece456cd39fab612")
+	assert.NoError(t, err)
+
+	// old code could produce a different error if called multiple times
+	for i := 0; i < 10; i++ {
+		_, err = commit.SubTree("file1.txt")
+		assert.Error(t, err)
+		assert.True(t, IsErrNotExist(err))
+	}
+}
diff --git a/modules/gitrepo/branch.go b/modules/gitrepo/branch.go
index dcd9c391ef..bb79fb95ca 100644
--- a/modules/gitrepo/branch.go
+++ b/modules/gitrepo/branch.go
@@ -50,3 +50,20 @@ func IsBranchExist(ctx context.Context, repo Repository, name string) bool {
 func IsWikiBranchExist(ctx context.Context, repo Repository, name string) bool {
 	return IsWikiReferenceExist(ctx, repo, git.BranchPrefix+name)
 }
+
+// SetDefaultBranch sets default branch of repository.
+func SetDefaultBranch(ctx context.Context, repo Repository, name string) error {
+	_, _, err := git.NewCommand(ctx, "symbolic-ref", "HEAD").
+		AddDynamicArguments(git.BranchPrefix + name).
+		RunStdString(&git.RunOpts{Dir: repoPath(repo)})
+	return err
+}
+
+// GetDefaultBranch gets default branch of repository.
+func GetDefaultBranch(ctx context.Context, repo Repository) (string, error) {
+	return git.GetDefaultBranch(ctx, repoPath(repo))
+}
+
+func GetWikiDefaultBranch(ctx context.Context, repo Repository) (string, error) {
+	return git.GetDefaultBranch(ctx, wikiPath(repo))
+}
diff --git a/modules/gitrepo/hooks.go b/modules/gitrepo/hooks.go
index 9f983247d4..5239fa4384 100644
--- a/modules/gitrepo/hooks.go
+++ b/modules/gitrepo/hooks.go
@@ -10,7 +10,6 @@ import (
 	"path/filepath"
 	"runtime"
 
-	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 )
@@ -95,15 +94,14 @@ done
 `, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)),
 	}
 
-	if git.SupportProcReceive {
-		hookNames = append(hookNames, "proc-receive")
-		hookTpls = append(hookTpls,
-			fmt.Sprintf(`#!/usr/bin/env %s
+	// although only new git (>=2.29) supports proc-receive, it's still good to create its hook, in case the user upgrades git
+	hookNames = append(hookNames, "proc-receive")
+	hookTpls = append(hookTpls,
+		fmt.Sprintf(`#!/usr/bin/env %s
 # AUTO GENERATED BY GITEA, DO NOT MODIFY
 %s hook --config=%s proc-receive
 `, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)))
-		giteaHookTpls = append(giteaHookTpls, "")
-	}
+	giteaHookTpls = append(giteaHookTpls, "")
 
 	return hookNames, hookTpls, giteaHookTpls
 }
diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go
index f3f412863a..3f1115066a 100644
--- a/modules/graceful/manager.go
+++ b/modules/graceful/manager.go
@@ -233,7 +233,10 @@ func (g *Manager) setStateTransition(old, new state) bool {
 // At the moment the total number of servers (numberOfServersToCreate) are pre-defined as a const before global init,
 // so this function MUST be called if a server is not used.
 func (g *Manager) InformCleanup() {
-	g.createServerWaitGroup.Done()
+	g.createServerCond.L.Lock()
+	defer g.createServerCond.L.Unlock()
+	g.createdServer++
+	g.createServerCond.Signal()
 }
 
 // Done allows the manager to be viewed as a context.Context, it returns a channel that is closed when the server is finished terminating
diff --git a/modules/graceful/manager_common.go b/modules/graceful/manager_common.go
index 27196e1531..f6dbcc748d 100644
--- a/modules/graceful/manager_common.go
+++ b/modules/graceful/manager_common.go
@@ -42,8 +42,9 @@ type Manager struct {
 	terminateCtxCancel     context.CancelFunc
 	managerCtxCancel       context.CancelFunc
 	runningServerWaitGroup sync.WaitGroup
-	createServerWaitGroup  sync.WaitGroup
 	terminateWaitGroup     sync.WaitGroup
+	createServerCond       sync.Cond
+	createdServer          int
 	shutdownRequested      chan struct{}
 
 	toRunAtShutdown  []func()
@@ -52,7 +53,7 @@ type Manager struct {
 
 func newGracefulManager(ctx context.Context) *Manager {
 	manager := &Manager{ctx: ctx, shutdownRequested: make(chan struct{})}
-	manager.createServerWaitGroup.Add(numberOfServersToCreate)
+	manager.createServerCond.L = &sync.Mutex{}
 	manager.prepare(ctx)
 	manager.start()
 	return manager
diff --git a/modules/graceful/manager_unix.go b/modules/graceful/manager_unix.go
index f4af4993d9..d03fff9b5b 100644
--- a/modules/graceful/manager_unix.go
+++ b/modules/graceful/manager_unix.go
@@ -57,12 +57,27 @@ func (g *Manager) start() {
 	// Handle clean up of unused provided listeners	and delayed start-up
 	startupDone := make(chan struct{})
 	go func() {
-		defer close(startupDone)
-		// Wait till we're done getting all the listeners and then close the unused ones
-		g.createServerWaitGroup.Wait()
-		// Ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function
-		_ = CloseProvidedListeners()
-		g.notify(readyMsg)
+		defer func() {
+			close(startupDone)
+			// Close the unused listeners
+			closeProvidedListeners()
+		}()
+		// Wait for all servers to be created
+		g.createServerCond.L.Lock()
+		for {
+			if g.createdServer >= numberOfServersToCreate {
+				g.createServerCond.L.Unlock()
+				g.notify(readyMsg)
+				return
+			}
+			select {
+			case <-g.IsShutdown():
+				g.createServerCond.L.Unlock()
+				return
+			default:
+			}
+			g.createServerCond.Wait()
+		}
 	}()
 	if setting.StartupTimeout > 0 {
 		go func() {
@@ -70,16 +85,7 @@ func (g *Manager) start() {
 			case <-startupDone:
 				return
 			case <-g.IsShutdown():
-				func() {
-					// When WaitGroup counter goes negative it will panic - we don't care about this so we can just ignore it.
-					defer func() {
-						_ = recover()
-					}()
-					// Ensure that the createServerWaitGroup stops waiting
-					for {
-						g.createServerWaitGroup.Done()
-					}
-				}()
+				g.createServerCond.Signal()
 				return
 			case <-time.After(setting.StartupTimeout):
 				log.Error("Startup took too long! Shutting down")
diff --git a/modules/graceful/manager_windows.go b/modules/graceful/manager_windows.go
index 0248dcb24d..d776e0e9f9 100644
--- a/modules/graceful/manager_windows.go
+++ b/modules/graceful/manager_windows.go
@@ -149,25 +149,35 @@ hammerLoop:
 func (g *Manager) awaitServer(limit time.Duration) bool {
 	c := make(chan struct{})
 	go func() {
-		defer close(c)
-		g.createServerWaitGroup.Wait()
+		g.createServerCond.L.Lock()
+		for {
+			if g.createdServer >= numberOfServersToCreate {
+				g.createServerCond.L.Unlock()
+				close(c)
+				return
+			}
+			select {
+			case <-g.IsShutdown():
+				g.createServerCond.L.Unlock()
+				return
+			default:
+			}
+			g.createServerCond.Wait()
+		}
 	}()
+
+	var tc <-chan time.Time
 	if limit > 0 {
-		select {
-		case <-c:
-			return true // completed normally
-		case <-time.After(limit):
-			return false // timed out
-		case <-g.IsShutdown():
-			return false
-		}
-	} else {
-		select {
-		case <-c:
-			return true // completed normally
-		case <-g.IsShutdown():
-			return false
-		}
+		tc = time.After(limit)
+	}
+	select {
+	case <-c:
+		return true // completed normally
+	case <-tc:
+		return false // timed out
+	case <-g.IsShutdown():
+		g.createServerCond.Signal()
+		return false
 	}
 }
 
diff --git a/modules/graceful/net_unix.go b/modules/graceful/net_unix.go
index 4f8c036a69..796e00507c 100644
--- a/modules/graceful/net_unix.go
+++ b/modules/graceful/net_unix.go
@@ -129,25 +129,17 @@ func getProvidedFDs() (savedErr error) {
 	return savedErr
 }
 
-// CloseProvidedListeners closes all unused provided listeners.
-func CloseProvidedListeners() error {
+// closeProvidedListeners closes all unused provided listeners.
+func closeProvidedListeners() {
 	mutex.Lock()
 	defer mutex.Unlock()
-	var returnableError error
 	for _, l := range providedListeners {
 		err := l.Close()
 		if err != nil {
 			log.Error("Error in closing unused provided listener: %v", err)
-			if returnableError != nil {
-				returnableError = fmt.Errorf("%v & %w", returnableError, err)
-			} else {
-				returnableError = err
-			}
 		}
 	}
 	providedListeners = []net.Listener{}
-
-	return returnableError
 }
 
 // DefaultGetListener obtains a listener for the stream-oriented local network address:
diff --git a/modules/httplib/url.go b/modules/httplib/url.go
index 14b95898f5..903799cb68 100644
--- a/modules/httplib/url.go
+++ b/modules/httplib/url.go
@@ -8,20 +8,42 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 )
 
-// IsRiskyRedirectURL returns true if the URL is considered risky for redirects
-func IsRiskyRedirectURL(s string) bool {
+func urlIsRelative(s string, u *url.URL) bool {
 	// Unfortunately browsers consider a redirect Location with preceding "//", "\\", "/\" and "\/" as meaning redirect to "http(s)://REST_OF_PATH"
 	// Therefore we should ignore these redirect locations to prevent open redirects
 	if len(s) > 1 && (s[0] == '/' || s[0] == '\\') && (s[1] == '/' || s[1] == '\\') {
-		return true
+		return false
 	}
-
-	u, err := url.Parse(s)
-	if err != nil || ((u.Scheme != "" || u.Host != "") && !strings.HasPrefix(strings.ToLower(s), strings.ToLower(setting.AppURL))) {
-		return true
-	}
-
-	return false
+	return u != nil && u.Scheme == "" && u.Host == ""
+}
+
+// IsRelativeURL detects if a URL is relative (no scheme or host)
+func IsRelativeURL(s string) bool {
+	u, err := url.Parse(s)
+	return err == nil && urlIsRelative(s, u)
+}
+
+func IsCurrentGiteaSiteURL(s string) bool {
+	u, err := url.Parse(s)
+	if err != nil {
+		return false
+	}
+	if u.Path != "" {
+		cleanedPath := util.PathJoinRelX(u.Path)
+		if cleanedPath == "" || cleanedPath == "." {
+			u.Path = "/"
+		} else {
+			u.Path += "/" + cleanedPath + "/"
+		}
+	}
+	if urlIsRelative(s, u) {
+		return u.Path == "" || strings.HasPrefix(strings.ToLower(u.Path), strings.ToLower(setting.AppSubURL+"/"))
+	}
+	if u.Path == "" {
+		u.Path = "/"
+	}
+	return strings.HasPrefix(strings.ToLower(u.String()), strings.ToLower(setting.AppURL))
 }
diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go
index 72033b1208..9bf09bcf2f 100644
--- a/modules/httplib/url_test.go
+++ b/modules/httplib/url_test.go
@@ -7,32 +7,70 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
 
 	"github.com/stretchr/testify/assert"
 )
 
-func TestIsRiskyRedirectURL(t *testing.T) {
-	setting.AppURL = "http://localhost:3000/"
-	tests := []struct {
-		input string
-		want  bool
-	}{
-		{"", false},
-		{"foo", false},
-		{"/", false},
-		{"/foo?k=%20#abc", false},
-
-		{"//", true},
-		{"\\\\", true},
-		{"/\\", true},
-		{"\\/", true},
-		{"mail:a@b.com", true},
-		{"https://test.com", true},
-		{setting.AppURL + "/foo", false},
+func TestIsRelativeURL(t *testing.T) {
+	defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
+	defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
+	rel := []string{
+		"",
+		"foo",
+		"/",
+		"/foo?k=%20#abc",
 	}
-	for _, tt := range tests {
-		t.Run(tt.input, func(t *testing.T) {
-			assert.Equal(t, tt.want, IsRiskyRedirectURL(tt.input))
-		})
+	for _, s := range rel {
+		assert.True(t, IsRelativeURL(s), "rel = %q", s)
+	}
+	abs := []string{
+		"//",
+		"\\\\",
+		"/\\",
+		"\\/",
+		"mailto:a@b.com",
+		"https://test.com",
+	}
+	for _, s := range abs {
+		assert.False(t, IsRelativeURL(s), "abs = %q", s)
 	}
 }
+
+func TestIsCurrentGiteaSiteURL(t *testing.T) {
+	defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
+	defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
+	good := []string{
+		"?key=val",
+		"/sub",
+		"/sub/",
+		"/sub/foo",
+		"/sub/foo/",
+		"http://localhost:3000/sub?key=val",
+		"http://localhost:3000/sub/",
+	}
+	for _, s := range good {
+		assert.True(t, IsCurrentGiteaSiteURL(s), "good = %q", s)
+	}
+	bad := []string{
+		".",
+		"foo",
+		"/",
+		"//",
+		"\\\\",
+		"/foo",
+		"http://localhost:3000/sub/..",
+		"http://localhost:3000/other",
+		"http://other/",
+	}
+	for _, s := range bad {
+		assert.False(t, IsCurrentGiteaSiteURL(s), "bad = %q", s)
+	}
+
+	setting.AppURL = "http://localhost:3000/"
+	setting.AppSubURL = ""
+	assert.False(t, IsCurrentGiteaSiteURL("//"))
+	assert.False(t, IsCurrentGiteaSiteURL("\\\\"))
+	assert.False(t, IsCurrentGiteaSiteURL("http://localhost"))
+	assert.True(t, IsCurrentGiteaSiteURL("http://localhost:3000?key=val"))
+}
diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go
index 8ba50ed77c..c607d780ef 100644
--- a/modules/indexer/code/bleve/bleve.go
+++ b/modules/indexer/code/bleve/bleve.go
@@ -39,6 +39,8 @@ import (
 const (
 	unicodeNormalizeName = "unicodeNormalize"
 	maxBatchSize         = 16
+	// fuzzyDenominator determines the levenshtein distance per each character of a keyword
+	fuzzyDenominator = 4
 )
 
 func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error {
@@ -142,7 +144,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro
 			return err
 		}
 		if size, err = strconv.ParseInt(strings.TrimSpace(stdout), 10, 64); err != nil {
-			return fmt.Errorf("Misformatted git cat-file output: %w", err)
+			return fmt.Errorf("misformatted git cat-file output: %w", err)
 		}
 	}
 
@@ -233,26 +235,23 @@ func (b *Indexer) Delete(_ context.Context, repoID int64) error {
 
 // Search searches for files in the specified repo.
 // Returns the matching file-paths
-func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
+func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
 	var (
 		indexerQuery query.Query
 		keywordQuery query.Query
 	)
 
-	if isMatch {
-		prefixQuery := bleve.NewPrefixQuery(keyword)
-		prefixQuery.FieldVal = "Content"
-		keywordQuery = prefixQuery
-	} else {
-		phraseQuery := bleve.NewMatchPhraseQuery(keyword)
-		phraseQuery.FieldVal = "Content"
-		phraseQuery.Analyzer = repoIndexerAnalyzer
-		keywordQuery = phraseQuery
+	phraseQuery := bleve.NewMatchPhraseQuery(opts.Keyword)
+	phraseQuery.FieldVal = "Content"
+	phraseQuery.Analyzer = repoIndexerAnalyzer
+	keywordQuery = phraseQuery
+	if opts.IsKeywordFuzzy {
+		phraseQuery.Fuzziness = len(opts.Keyword) / fuzzyDenominator
 	}
 
-	if len(repoIDs) > 0 {
-		repoQueries := make([]query.Query, 0, len(repoIDs))
-		for _, repoID := range repoIDs {
+	if len(opts.RepoIDs) > 0 {
+		repoQueries := make([]query.Query, 0, len(opts.RepoIDs))
+		for _, repoID := range opts.RepoIDs {
 			repoQueries = append(repoQueries, inner_bleve.NumericEqualityQuery(repoID, "RepoID"))
 		}
 
@@ -266,8 +265,8 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
 
 	// Save for reuse without language filter
 	facetQuery := indexerQuery
-	if len(language) > 0 {
-		languageQuery := bleve.NewMatchQuery(language)
+	if len(opts.Language) > 0 {
+		languageQuery := bleve.NewMatchQuery(opts.Language)
 		languageQuery.FieldVal = "Language"
 		languageQuery.Analyzer = analyzer_keyword.Name
 
@@ -277,12 +276,12 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
 		)
 	}
 
-	from := (page - 1) * pageSize
+	from, pageSize := opts.GetSkipTake()
 	searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false)
 	searchRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"}
 	searchRequest.IncludeLocations = true
 
-	if len(language) == 0 {
+	if len(opts.Language) == 0 {
 		searchRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10))
 	}
 
@@ -326,7 +325,7 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
 	}
 
 	searchResultLanguages := make([]*internal.SearchResultLanguages, 0, 10)
-	if len(language) > 0 {
+	if len(opts.Language) > 0 {
 		// Use separate query to go get all language counts
 		facetRequest := bleve.NewSearchRequestOptions(facetQuery, 1, 0, false)
 		facetRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"}
diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go
index 0f70f13485..e4622fd66e 100644
--- a/modules/indexer/code/elasticsearch/elasticsearch.go
+++ b/modules/indexer/code/elasticsearch/elasticsearch.go
@@ -281,18 +281,18 @@ func extractAggs(searchResult *elastic.SearchResult) []*internal.SearchResultLan
 }
 
 // Search searches for codes and language stats by given conditions.
-func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
-	searchType := esMultiMatchTypeBestFields
-	if isMatch {
-		searchType = esMultiMatchTypePhrasePrefix
+func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
+	searchType := esMultiMatchTypePhrasePrefix
+	if opts.IsKeywordFuzzy {
+		searchType = esMultiMatchTypeBestFields
 	}
 
-	kwQuery := elastic.NewMultiMatchQuery(keyword, "content").Type(searchType)
+	kwQuery := elastic.NewMultiMatchQuery(opts.Keyword, "content").Type(searchType)
 	query := elastic.NewBoolQuery()
 	query = query.Must(kwQuery)
-	if len(repoIDs) > 0 {
-		repoStrs := make([]any, 0, len(repoIDs))
-		for _, repoID := range repoIDs {
+	if len(opts.RepoIDs) > 0 {
+		repoStrs := make([]any, 0, len(opts.RepoIDs))
+		for _, repoID := range opts.RepoIDs {
 			repoStrs = append(repoStrs, repoID)
 		}
 		repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...)
@@ -300,16 +300,12 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
 	}
 
 	var (
-		start       int
-		kw          = "<em>" + keyword + "</em>"
-		aggregation = elastic.NewTermsAggregation().Field("language").Size(10).OrderByCountDesc()
+		start, pageSize = opts.GetSkipTake()
+		kw              = "<em>" + opts.Keyword + "</em>"
+		aggregation     = elastic.NewTermsAggregation().Field("language").Size(10).OrderByCountDesc()
 	)
 
-	if page > 0 {
-		start = (page - 1) * pageSize
-	}
-
-	if len(language) == 0 {
+	if len(opts.Language) == 0 {
 		searchResult, err := b.inner.Client.Search().
 			Index(b.inner.VersionedIndexName()).
 			Aggregation("language", aggregation).
@@ -330,7 +326,7 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
 		return convertResult(searchResult, kw, pageSize)
 	}
 
-	langQuery := elastic.NewMatchQuery("language", language)
+	langQuery := elastic.NewMatchQuery("language", opts.Language)
 	countResult, err := b.inner.Client.Search().
 		Index(b.inner.VersionedIndexName()).
 		Aggregation("language", aggregation).
diff --git a/modules/indexer/code/git.go b/modules/indexer/code/git.go
index 942861e8c0..2905a540e5 100644
--- a/modules/indexer/code/git.go
+++ b/modules/indexer/code/git.go
@@ -10,7 +10,6 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/indexer/code/internal"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
@@ -33,7 +32,7 @@ func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision s
 
 	needGenesis := len(status.CommitSha) == 0
 	if !needGenesis {
-		hasAncestorCmd := git.NewCommand(ctx, "merge-base").AddDynamicArguments(repo.CodeIndexerStatus.CommitSha, revision)
+		hasAncestorCmd := git.NewCommand(ctx, "merge-base").AddDynamicArguments(status.CommitSha, revision)
 		stdout, _, _ := hasAncestorCmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
 		needGenesis = len(stdout) == 0
 	}
@@ -92,11 +91,9 @@ func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision s
 		return nil, runErr
 	}
 
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+
 	var err error
-	objectFormat, err := gitrepo.GetObjectFormatOfRepo(ctx, repo)
-	if err != nil {
-		return nil, err
-	}
 	changes.Updates, err = parseGitLsTreeOutput(objectFormat, stdout)
 	return &changes, err
 }
@@ -175,10 +172,8 @@ func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revisio
 		return nil, err
 	}
 
-	objectFormat, err := gitrepo.GetObjectFormatOfRepo(ctx, repo)
-	if err != nil {
-		return nil, err
-	}
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+
 	changes.Updates, err = parseGitLsTreeOutput(objectFormat, lsTreeStdout)
 	return &changes, err
 }
diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go
index 5eb8e61e3d..8975c5ce40 100644
--- a/modules/indexer/code/indexer_test.go
+++ b/modules/indexer/code/indexer_test.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/indexer/code/bleve"
@@ -70,7 +71,15 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
 
 		for _, kw := range keywords {
 			t.Run(kw.Keyword, func(t *testing.T) {
-				total, res, langs, err := indexer.Search(context.TODO(), kw.RepoIDs, "", kw.Keyword, 1, 10, false)
+				total, res, langs, err := indexer.Search(context.TODO(), &internal.SearchOptions{
+					RepoIDs: kw.RepoIDs,
+					Keyword: kw.Keyword,
+					Paginator: &db.ListOptions{
+						Page:     1,
+						PageSize: 10,
+					},
+					IsKeywordFuzzy: true,
+				})
 				assert.NoError(t, err)
 				assert.Len(t, kw.IDs, int(total))
 				assert.Len(t, langs, kw.Langs)
diff --git a/modules/indexer/code/internal/indexer.go b/modules/indexer/code/internal/indexer.go
index da3ac3623c..c259fcd26e 100644
--- a/modules/indexer/code/internal/indexer.go
+++ b/modules/indexer/code/internal/indexer.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"fmt"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/indexer/internal"
 )
@@ -16,7 +17,17 @@ type Indexer interface {
 	internal.Indexer
 	Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *RepoChanges) error
 	Delete(ctx context.Context, repoID int64) error
-	Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error)
+	Search(ctx context.Context, opts *SearchOptions) (int64, []*SearchResult, []*SearchResultLanguages, error)
+}
+
+type SearchOptions struct {
+	RepoIDs  []int64
+	Keyword  string
+	Language string
+
+	IsKeywordFuzzy bool
+
+	db.Paginator
 }
 
 // NewDummyIndexer returns a dummy indexer
@@ -38,6 +49,6 @@ func (d *dummyIndexer) Delete(ctx context.Context, repoID int64) error {
 	return fmt.Errorf("indexer is not ready")
 }
 
-func (d *dummyIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) {
+func (d *dummyIndexer) Search(ctx context.Context, opts *SearchOptions) (int64, []*SearchResult, []*SearchResultLanguages, error) {
 	return 0, nil, nil, fmt.Errorf("indexer is not ready")
 }
diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go
index e19e22eea0..74c957dde6 100644
--- a/modules/indexer/code/search.go
+++ b/modules/indexer/code/search.go
@@ -16,18 +16,24 @@ import (
 
 // Result a search result to display
 type Result struct {
-	RepoID         int64
-	Filename       string
-	CommitID       string
-	UpdatedUnix    timeutil.TimeStamp
-	Language       string
-	Color          string
-	LineNumbers    []int
-	FormattedLines template.HTML
+	RepoID      int64
+	Filename    string
+	CommitID    string
+	UpdatedUnix timeutil.TimeStamp
+	Language    string
+	Color       string
+	Lines       []*ResultLine
+}
+
+type ResultLine struct {
+	Num              int
+	FormattedContent template.HTML
 }
 
 type SearchResultLanguages = internal.SearchResultLanguages
 
+type SearchOptions = internal.SearchOptions
+
 func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) {
 	startIndex := selectionStartIndex
 	numLinesBefore := 0
@@ -64,13 +70,29 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error {
 	return nil
 }
 
+func HighlightSearchResultCode(filename, language string, lineNums []int, code string) []*ResultLine {
+	// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
+	hl, _ := highlight.Code(filename, language, code)
+	highlightedLines := strings.Split(string(hl), "\n")
+
+	// The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n`
+	lines := make([]*ResultLine, min(len(highlightedLines), len(lineNums)))
+	for i := 0; i < len(lines); i++ {
+		lines[i] = &ResultLine{
+			Num:              lineNums[i],
+			FormattedContent: template.HTML(highlightedLines[i]),
+		}
+	}
+	return lines
+}
+
 func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Result, error) {
 	startLineNum := 1 + strings.Count(result.Content[:startIndex], "\n")
 
 	var formattedLinesBuffer bytes.Buffer
 
 	contentLines := strings.SplitAfter(result.Content[startIndex:endIndex], "\n")
-	lineNumbers := make([]int, len(contentLines))
+	lineNums := make([]int, 0, len(contentLines))
 	index := startIndex
 	for i, line := range contentLines {
 		var err error
@@ -85,39 +107,35 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
 				line[closeActiveIndex:],
 			)
 		} else {
-			err = writeStrings(&formattedLinesBuffer,
-				line,
-			)
+			err = writeStrings(&formattedLinesBuffer, line)
 		}
 		if err != nil {
 			return nil, err
 		}
 
-		lineNumbers[i] = startLineNum + i
+		lineNums = append(lineNums, startLineNum+i)
 		index += len(line)
 	}
 
-	highlighted, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String())
-
 	return &Result{
-		RepoID:         result.RepoID,
-		Filename:       result.Filename,
-		CommitID:       result.CommitID,
-		UpdatedUnix:    result.UpdatedUnix,
-		Language:       result.Language,
-		Color:          result.Color,
-		LineNumbers:    lineNumbers,
-		FormattedLines: highlighted,
+		RepoID:      result.RepoID,
+		Filename:    result.Filename,
+		CommitID:    result.CommitID,
+		UpdatedUnix: result.UpdatedUnix,
+		Language:    result.Language,
+		Color:       result.Color,
+		Lines:       HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String()),
 	}, nil
 }
 
 // PerformSearch perform a search on a repository
-func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int, []*Result, []*internal.SearchResultLanguages, error) {
-	if len(keyword) == 0 {
+// if isFuzzy is true set the Damerau-Levenshtein distance from 0 to 2
+func PerformSearch(ctx context.Context, opts *SearchOptions) (int, []*Result, []*SearchResultLanguages, error) {
+	if opts == nil || len(opts.Keyword) == 0 {
 		return 0, nil, nil, nil
 	}
 
-	total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, repoIDs, language, keyword, page, pageSize, isMatch)
+	total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, opts)
 	if err != nil {
 		return 0, nil, nil, err
 	}
diff --git a/modules/indexer/internal/bleve/indexer.go b/modules/indexer/internal/bleve/indexer.go
index ce06b5afcb..01e53ca636 100644
--- a/modules/indexer/internal/bleve/indexer.go
+++ b/modules/indexer/internal/bleve/indexer.go
@@ -92,7 +92,7 @@ func (i *Indexer) Ping(_ context.Context) error {
 }
 
 func (i *Indexer) Close() {
-	if i == nil {
+	if i == nil || i.Indexer == nil {
 		return
 	}
 
diff --git a/modules/indexer/internal/bleve/query.go b/modules/indexer/internal/bleve/query.go
index c7d66538c1..21422b281c 100644
--- a/modules/indexer/internal/bleve/query.go
+++ b/modules/indexer/internal/bleve/query.go
@@ -4,6 +4,8 @@
 package bleve
 
 import (
+	"code.gitea.io/gitea/modules/optional"
+
 	"github.com/blevesearch/bleve/v2"
 	"github.com/blevesearch/bleve/v2/search/query"
 )
@@ -18,10 +20,11 @@ func NumericEqualityQuery(value int64, field string) *query.NumericRangeQuery {
 }
 
 // MatchPhraseQuery generates a match phrase query for the given phrase, field and analyzer
-func MatchPhraseQuery(matchPhrase, field, analyzer string) *query.MatchPhraseQuery {
+func MatchPhraseQuery(matchPhrase, field, analyzer string, fuzziness int) *query.MatchPhraseQuery {
 	q := bleve.NewMatchPhraseQuery(matchPhrase)
 	q.FieldVal = field
 	q.Analyzer = analyzer
+	q.Fuzziness = fuzziness
 	return q
 }
 
@@ -32,18 +35,18 @@ func BoolFieldQuery(value bool, field string) *query.BoolFieldQuery {
 	return q
 }
 
-func NumericRangeInclusiveQuery(min, max *int64, field string) *query.NumericRangeQuery {
+func NumericRangeInclusiveQuery(min, max optional.Option[int64], field string) *query.NumericRangeQuery {
 	var minF, maxF *float64
 	var minI, maxI *bool
-	if min != nil {
+	if min.Has() {
 		minF = new(float64)
-		*minF = float64(*min)
+		*minF = float64(min.Value())
 		minI = new(bool)
 		*minI = true
 	}
-	if max != nil {
+	if max.Has() {
 		maxF = new(float64)
-		*maxF = float64(*max)
+		*maxF = float64(max.Value())
 		maxI = new(bool)
 		*maxI = true
 	}
diff --git a/modules/indexer/internal/meilisearch/indexer.go b/modules/indexer/internal/meilisearch/indexer.go
index b037249d43..f4004849c1 100644
--- a/modules/indexer/internal/meilisearch/indexer.go
+++ b/modules/indexer/internal/meilisearch/indexer.go
@@ -87,8 +87,5 @@ func (i *Indexer) Close() {
 	if i == nil {
 		return
 	}
-	if i.Client == nil {
-		return
-	}
 	i.Client = nil
 }
diff --git a/modules/indexer/internal/paginator.go b/modules/indexer/internal/paginator.go
index de0a33c06f..ee204bf047 100644
--- a/modules/indexer/internal/paginator.go
+++ b/modules/indexer/internal/paginator.go
@@ -10,7 +10,7 @@ import (
 )
 
 // ParsePaginator parses a db.Paginator into a skip and limit
-func ParsePaginator(paginator db.Paginator, max ...int) (int, int) {
+func ParsePaginator(paginator *db.ListOptions, max ...int) (int, int) {
 	// Use a very large number to indicate no limit
 	unlimited := math.MaxInt32
 	if len(max) > 0 {
@@ -19,22 +19,15 @@ func ParsePaginator(paginator db.Paginator, max ...int) (int, int) {
 	}
 
 	if paginator == nil || paginator.IsListAll() {
+		// It shouldn't happen. In actual usage scenarios, there should not be requests to search all.
+		// But if it does happen, respect it and return "unlimited".
+		// And it's also useful for testing.
 		return 0, unlimited
 	}
 
-	// Warning: Do not use GetSkipTake() for *db.ListOptions
-	// Its implementation could reset the page size with setting.API.MaxResponseItems
-	if listOptions, ok := paginator.(*db.ListOptions); ok {
-		if listOptions.Page >= 0 && listOptions.PageSize > 0 {
-			var start int
-			if listOptions.Page == 0 {
-				start = 0
-			} else {
-				start = (listOptions.Page - 1) * listOptions.PageSize
-			}
-			return start, listOptions.PageSize
-		}
-		return 0, unlimited
+	if paginator.PageSize == 0 {
+		// Do not return any results when searching, it's used to get the total count only.
+		return 0, 0
 	}
 
 	return paginator.GetSkipTake()
diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go
index 7c82cfbb79..1f54be721b 100644
--- a/modules/indexer/issues/bleve/bleve.go
+++ b/modules/indexer/issues/bleve/bleve.go
@@ -35,7 +35,11 @@ func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error {
 	})
 }
 
-const maxBatchSize = 16
+const (
+	maxBatchSize = 16
+	// fuzzyDenominator determines the levenshtein distance per each character of a keyword
+	fuzzyDenominator = 4
+)
 
 // IndexerData an update to the issue indexer
 type IndexerData internal.IndexerData
@@ -156,12 +160,16 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 	var queries []query.Query
 
 	if options.Keyword != "" {
-		keywordQueries := []query.Query{
-			inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer),
-			inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer),
-			inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer),
+		fuzziness := 0
+		if options.IsFuzzyKeyword {
+			fuzziness = len(options.Keyword) / fuzzyDenominator
 		}
-		queries = append(queries, bleve.NewDisjunctionQuery(keywordQueries...))
+
+		queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{
+			inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer, fuzziness),
+			inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer, fuzziness),
+			inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer, fuzziness),
+		}...))
 	}
 
 	if len(options.RepoIDs) > 0 || options.AllPublic {
@@ -175,11 +183,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		queries = append(queries, bleve.NewDisjunctionQuery(repoQueries...))
 	}
 
-	if !options.IsPull.IsNone() {
-		queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.IsTrue(), "is_pull"))
+	if options.IsPull.Has() {
+		queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.Value(), "is_pull"))
 	}
-	if !options.IsClosed.IsNone() {
-		queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.IsTrue(), "is_closed"))
+	if options.IsClosed.Has() {
+		queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.Value(), "is_closed"))
 	}
 
 	if options.NoLabelOnly {
@@ -217,38 +225,41 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		queries = append(queries, bleve.NewDisjunctionQuery(milestoneQueries...))
 	}
 
-	if options.ProjectID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ProjectID, "project_id"))
+	if options.ProjectID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id"))
 	}
-	if options.ProjectBoardID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ProjectBoardID, "project_board_id"))
+	if options.ProjectBoardID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectBoardID.Value(), "project_board_id"))
 	}
 
-	if options.PosterID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.PosterID, "poster_id"))
+	if options.PosterID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.PosterID.Value(), "poster_id"))
 	}
 
-	if options.AssigneeID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.AssigneeID, "assignee_id"))
+	if options.AssigneeID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.AssigneeID.Value(), "assignee_id"))
 	}
 
-	if options.MentionID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.MentionID, "mention_ids"))
+	if options.MentionID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.MentionID.Value(), "mention_ids"))
 	}
 
-	if options.ReviewedID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ReviewedID, "reviewed_ids"))
+	if options.ReviewedID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.ReviewedID.Value(), "reviewed_ids"))
 	}
-	if options.ReviewRequestedID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ReviewRequestedID, "review_requested_ids"))
+	if options.ReviewRequestedID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.ReviewRequestedID.Value(), "review_requested_ids"))
 	}
 
-	if options.SubscriberID != nil {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(*options.SubscriberID, "subscriber_ids"))
+	if options.SubscriberID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.SubscriberID.Value(), "subscriber_ids"))
 	}
 
-	if options.UpdatedAfterUnix != nil || options.UpdatedBeforeUnix != nil {
-		queries = append(queries, inner_bleve.NumericRangeInclusiveQuery(options.UpdatedAfterUnix, options.UpdatedBeforeUnix, "updated_unix"))
+	if options.UpdatedAfterUnix.Has() || options.UpdatedBeforeUnix.Has() {
+		queries = append(queries, inner_bleve.NumericRangeInclusiveQuery(
+			options.UpdatedAfterUnix,
+			options.UpdatedBeforeUnix,
+			"updated_unix"))
 	}
 
 	var indexerQuery query.Query = bleve.NewConjunctionQuery(queries...)
diff --git a/modules/indexer/issues/db/db.go b/modules/indexer/issues/db/db.go
index 1016523b72..05ec548435 100644
--- a/modules/indexer/issues/db/db.go
+++ b/modules/indexer/issues/db/db.go
@@ -78,6 +78,17 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		return nil, err
 	}
 
+	// If pagesize == 0, return total count only. It's a special case for search count.
+	if options.Paginator != nil && options.Paginator.PageSize == 0 {
+		total, err := issue_model.CountIssues(ctx, opt, cond)
+		if err != nil {
+			return nil, err
+		}
+		return &internal.SearchResult{
+			Total: total,
+		}, nil
+	}
+
 	ids, total, err := issue_model.IssueIDs(ctx, opt, cond)
 	if err != nil {
 		return nil, err
diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go
index 5406715bbc..eeaf1696ad 100644
--- a/modules/indexer/issues/db/options.go
+++ b/modules/indexer/issues/db/options.go
@@ -11,25 +11,10 @@ import (
 	issue_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/indexer/issues/internal"
+	"code.gitea.io/gitea/modules/optional"
 )
 
 func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_model.IssuesOptions, error) {
-	// See the comment of issues_model.SearchOptions for the reason why we need to convert
-	convertID := func(id *int64) int64 {
-		if id == nil {
-			return 0
-		}
-		if *id == 0 {
-			return db.NoConditionID
-		}
-		return *id
-	}
-	convertInt64 := func(i *int64) int64 {
-		if i == nil {
-			return 0
-		}
-		return *i
-	}
 	var sortType string
 	switch options.SortBy {
 	case internal.SortByCreatedAsc:
@@ -52,6 +37,18 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
 		sortType = "newest"
 	}
 
+	// See the comment of issues_model.SearchOptions for the reason why we need to convert
+	convertID := func(id optional.Option[int64]) int64 {
+		if !id.Has() {
+			return 0
+		}
+		value := id.Value()
+		if value == 0 {
+			return db.NoConditionID
+		}
+		return value
+	}
+
 	opts := &issue_model.IssuesOptions{
 		Paginator:          options.Paginator,
 		RepoIDs:            options.RepoIDs,
@@ -72,10 +69,10 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
 		IncludeMilestones:  nil,
 		SortType:           sortType,
 		IssueIDs:           nil,
-		UpdatedAfterUnix:   convertInt64(options.UpdatedAfterUnix),
-		UpdatedBeforeUnix:  convertInt64(options.UpdatedBeforeUnix),
+		UpdatedAfterUnix:   options.UpdatedAfterUnix.Value(),
+		UpdatedBeforeUnix:  options.UpdatedBeforeUnix.Value(),
 		PriorityRepoID:     0,
-		IsArchived:         0,
+		IsArchived:         optional.None[bool](),
 		Org:                nil,
 		Team:               nil,
 		User:               nil,
diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go
index 80e233e29a..4a98b4588a 100644
--- a/modules/indexer/issues/dboptions.go
+++ b/modules/indexer/issues/dboptions.go
@@ -6,6 +6,7 @@ package issues
 import (
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/modules/optional"
 )
 
 func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOptions {
@@ -38,13 +39,12 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
 	}
 
 	// See the comment of issues_model.SearchOptions for the reason why we need to convert
-	convertID := func(id int64) *int64 {
+	convertID := func(id int64) optional.Option[int64] {
 		if id > 0 {
-			return &id
+			return optional.Some(id)
 		}
 		if id == db.NoConditionID {
-			var zero int64
-			return &zero
+			return optional.None[int64]()
 		}
 		return nil
 	}
@@ -59,10 +59,10 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
 	searchOpt.SubscriberID = convertID(opts.SubscriberID)
 
 	if opts.UpdatedAfterUnix > 0 {
-		searchOpt.UpdatedAfterUnix = &opts.UpdatedAfterUnix
+		searchOpt.UpdatedAfterUnix = optional.Some(opts.UpdatedAfterUnix)
 	}
 	if opts.UpdatedBeforeUnix > 0 {
-		searchOpt.UpdatedBeforeUnix = &opts.UpdatedBeforeUnix
+		searchOpt.UpdatedBeforeUnix = optional.Some(opts.UpdatedBeforeUnix)
 	}
 
 	searchOpt.Paginator = opts.Paginator
diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go
index d059f76b32..53b383c8d5 100644
--- a/modules/indexer/issues/elasticsearch/elasticsearch.go
+++ b/modules/indexer/issues/elasticsearch/elasticsearch.go
@@ -19,6 +19,10 @@ import (
 
 const (
 	issueIndexerLatestVersion = 1
+	// multi-match-types, currently only 2 types are used
+	// Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types
+	esMultiMatchTypeBestFields   = "best_fields"
+	esMultiMatchTypePhrasePrefix = "phrase_prefix"
 )
 
 var _ internal.Indexer = &Indexer{}
@@ -141,7 +145,13 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 	query := elastic.NewBoolQuery()
 
 	if options.Keyword != "" {
-		query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments"))
+
+		searchType := esMultiMatchTypePhrasePrefix
+		if options.IsFuzzyKeyword {
+			searchType = esMultiMatchTypeBestFields
+		}
+
+		query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments").Type(searchType))
 	}
 
 	if len(options.RepoIDs) > 0 {
@@ -153,11 +163,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		query.Must(q)
 	}
 
-	if !options.IsPull.IsNone() {
-		query.Must(elastic.NewTermQuery("is_pull", options.IsPull.IsTrue()))
+	if options.IsPull.Has() {
+		query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value()))
 	}
-	if !options.IsClosed.IsNone() {
-		query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.IsTrue()))
+	if options.IsClosed.Has() {
+		query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.Value()))
 	}
 
 	if options.NoLabelOnly {
@@ -185,43 +195,43 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		query.Must(elastic.NewTermsQuery("milestone_id", toAnySlice(options.MilestoneIDs)...))
 	}
 
-	if options.ProjectID != nil {
-		query.Must(elastic.NewTermQuery("project_id", *options.ProjectID))
+	if options.ProjectID.Has() {
+		query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value()))
 	}
-	if options.ProjectBoardID != nil {
-		query.Must(elastic.NewTermQuery("project_board_id", *options.ProjectBoardID))
+	if options.ProjectBoardID.Has() {
+		query.Must(elastic.NewTermQuery("project_board_id", options.ProjectBoardID.Value()))
 	}
 
-	if options.PosterID != nil {
-		query.Must(elastic.NewTermQuery("poster_id", *options.PosterID))
+	if options.PosterID.Has() {
+		query.Must(elastic.NewTermQuery("poster_id", options.PosterID.Value()))
 	}
 
-	if options.AssigneeID != nil {
-		query.Must(elastic.NewTermQuery("assignee_id", *options.AssigneeID))
+	if options.AssigneeID.Has() {
+		query.Must(elastic.NewTermQuery("assignee_id", options.AssigneeID.Value()))
 	}
 
-	if options.MentionID != nil {
-		query.Must(elastic.NewTermQuery("mention_ids", *options.MentionID))
+	if options.MentionID.Has() {
+		query.Must(elastic.NewTermQuery("mention_ids", options.MentionID.Value()))
 	}
 
-	if options.ReviewedID != nil {
-		query.Must(elastic.NewTermQuery("reviewed_ids", *options.ReviewedID))
+	if options.ReviewedID.Has() {
+		query.Must(elastic.NewTermQuery("reviewed_ids", options.ReviewedID.Value()))
 	}
-	if options.ReviewRequestedID != nil {
-		query.Must(elastic.NewTermQuery("review_requested_ids", *options.ReviewRequestedID))
+	if options.ReviewRequestedID.Has() {
+		query.Must(elastic.NewTermQuery("review_requested_ids", options.ReviewRequestedID.Value()))
 	}
 
-	if options.SubscriberID != nil {
-		query.Must(elastic.NewTermQuery("subscriber_ids", *options.SubscriberID))
+	if options.SubscriberID.Has() {
+		query.Must(elastic.NewTermQuery("subscriber_ids", options.SubscriberID.Value()))
 	}
 
-	if options.UpdatedAfterUnix != nil || options.UpdatedBeforeUnix != nil {
+	if options.UpdatedAfterUnix.Has() || options.UpdatedBeforeUnix.Has() {
 		q := elastic.NewRangeQuery("updated_unix")
-		if options.UpdatedAfterUnix != nil {
-			q.Gte(*options.UpdatedAfterUnix)
+		if options.UpdatedAfterUnix.Has() {
+			q.Gte(options.UpdatedAfterUnix.Value())
 		}
-		if options.UpdatedBeforeUnix != nil {
-			q.Lte(*options.UpdatedBeforeUnix)
+		if options.UpdatedBeforeUnix.Has() {
+			q.Lte(options.UpdatedBeforeUnix.Value())
 		}
 		query.Must(q)
 	}
diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go
index 57037d2947..1cb86feb82 100644
--- a/modules/indexer/issues/indexer.go
+++ b/modules/indexer/issues/indexer.go
@@ -20,10 +20,10 @@ import (
 	"code.gitea.io/gitea/modules/indexer/issues/internal"
 	"code.gitea.io/gitea/modules/indexer/issues/meilisearch"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/queue"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 )
 
 // IndexerMetadata is used to send data to the queue, so it contains only the ids.
@@ -220,7 +220,7 @@ func PopulateIssueIndexer(ctx context.Context) error {
 			ListOptions: db_model.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize},
 			OrderBy:     db_model.SearchOrderByID,
 			Private:     true,
-			Collaborate: util.OptionalBoolFalse,
+			Collaborate: optional.Some(false),
 		})
 		if err != nil {
 			log.Error("SearchRepositoryByName: %v", err)
@@ -308,7 +308,7 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
 
 // CountIssues counts issues by options. It is a shortcut of SearchIssues(ctx, opts) but only returns the total count.
 func CountIssues(ctx context.Context, opts *SearchOptions) (int64, error) {
-	opts = opts.Copy(func(options *SearchOptions) { opts.Paginator = &db_model.ListOptions{PageSize: 0} })
+	opts = opts.Copy(func(options *SearchOptions) { options.Paginator = &db_model.ListOptions{PageSize: 0} })
 
 	_, total, err := SearchIssues(ctx, opts)
 	return total, err
diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go
index da4fc9b878..0d0cfc8516 100644
--- a/modules/indexer/issues/indexer_test.go
+++ b/modules/indexer/issues/indexer_test.go
@@ -10,8 +10,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/indexer/issues/internal"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 
 	_ "code.gitea.io/gitea/models"
 	_ "code.gitea.io/gitea/models/actions"
@@ -134,63 +134,60 @@ func searchIssueInRepo(t *testing.T) {
 }
 
 func searchIssueByID(t *testing.T) {
-	int64Pointer := func(x int64) *int64 {
-		return &x
-	}
 	tests := []struct {
 		opts        SearchOptions
 		expectedIDs []int64
 	}{
 		{
-			SearchOptions{
-				PosterID: int64Pointer(1),
+			opts: SearchOptions{
+				PosterID: optional.Some(int64(1)),
 			},
-			[]int64{11, 6, 3, 2, 1},
+			expectedIDs: []int64{11, 6, 3, 2, 1},
 		},
 		{
-			SearchOptions{
-				AssigneeID: int64Pointer(1),
+			opts: SearchOptions{
+				AssigneeID: optional.Some(int64(1)),
 			},
-			[]int64{6, 1},
+			expectedIDs: []int64{6, 1},
 		},
 		{
-			SearchOptions{
-				MentionID: int64Pointer(4),
+			opts: SearchOptions{
+				MentionID: optional.Some(int64(4)),
 			},
-			[]int64{1},
+			expectedIDs: []int64{1},
 		},
 		{
-			SearchOptions{
-				ReviewedID: int64Pointer(1),
+			opts: SearchOptions{
+				ReviewedID: optional.Some(int64(1)),
 			},
-			[]int64{},
+			expectedIDs: []int64{},
 		},
 		{
-			SearchOptions{
-				ReviewRequestedID: int64Pointer(1),
+			opts: SearchOptions{
+				ReviewRequestedID: optional.Some(int64(1)),
 			},
-			[]int64{12},
+			expectedIDs: []int64{12},
 		},
 		{
-			SearchOptions{
-				SubscriberID: int64Pointer(1),
+			opts: SearchOptions{
+				SubscriberID: optional.Some(int64(1)),
 			},
-			[]int64{11, 6, 5, 3, 2, 1},
+			expectedIDs: []int64{11, 6, 5, 3, 2, 1},
 		},
 		{
 			// issue 20 request user 15 and team 5 which user 15 belongs to
 			// the review request number of issue 20 should be 1
-			SearchOptions{
-				ReviewRequestedID: int64Pointer(15),
+			opts: SearchOptions{
+				ReviewRequestedID: optional.Some(int64(15)),
 			},
-			[]int64{12, 20},
+			expectedIDs: []int64{12, 20},
 		},
 		{
 			// user 20 approved the issue 20, so return nothing
-			SearchOptions{
-				ReviewRequestedID: int64Pointer(20),
+			opts: SearchOptions{
+				ReviewRequestedID: optional.Some(int64(20)),
 			},
-			[]int64{},
+			expectedIDs: []int64{},
 		},
 	}
 
@@ -210,15 +207,15 @@ func searchIssueIsPull(t *testing.T) {
 	}{
 		{
 			SearchOptions{
-				IsPull: util.OptionalBoolFalse,
+				IsPull: optional.Some(false),
 			},
 			[]int64{17, 16, 15, 14, 13, 6, 5, 18, 10, 7, 4, 1},
 		},
 		{
 			SearchOptions{
-				IsPull: util.OptionalBoolTrue,
+				IsPull: optional.Some(true),
 			},
-			[]int64{12, 11, 20, 19, 9, 8, 3, 2},
+			[]int64{22, 21, 12, 11, 20, 19, 9, 8, 3, 2},
 		},
 	}
 	for _, test := range tests {
@@ -237,13 +234,13 @@ func searchIssueIsClosed(t *testing.T) {
 	}{
 		{
 			SearchOptions{
-				IsClosed: util.OptionalBoolFalse,
+				IsClosed: optional.Some(false),
 			},
-			[]int64{17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1},
+			[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1},
 		},
 		{
 			SearchOptions{
-				IsClosed: util.OptionalBoolTrue,
+				IsClosed: optional.Some(true),
 			},
 			[]int64{5, 4},
 		},
@@ -305,7 +302,7 @@ func searchIssueByLabelID(t *testing.T) {
 			SearchOptions{
 				ExcludedLabelIDs: []int64{1},
 			},
-			[]int64{17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3},
+			[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3},
 		},
 	}
 	for _, test := range tests {
@@ -318,18 +315,15 @@ func searchIssueByLabelID(t *testing.T) {
 }
 
 func searchIssueByTime(t *testing.T) {
-	int64Pointer := func(i int64) *int64 {
-		return &i
-	}
 	tests := []struct {
 		opts        SearchOptions
 		expectedIDs []int64
 	}{
 		{
 			SearchOptions{
-				UpdatedAfterUnix: int64Pointer(0),
+				UpdatedAfterUnix: optional.Some(int64(0)),
 			},
-			[]int64{17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1},
+			[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1},
 		},
 	}
 	for _, test := range tests {
@@ -350,7 +344,7 @@ func searchIssueWithOrder(t *testing.T) {
 			SearchOptions{
 				SortBy: internal.SortByCreatedAsc,
 			},
-			[]int64{1, 2, 3, 8, 9, 4, 7, 10, 18, 19, 5, 6, 20, 11, 12, 13, 14, 15, 16, 17},
+			[]int64{1, 2, 3, 8, 9, 4, 7, 10, 18, 19, 5, 6, 20, 11, 12, 13, 14, 15, 16, 17, 21, 22},
 		},
 	}
 	for _, test := range tests {
@@ -363,28 +357,25 @@ func searchIssueWithOrder(t *testing.T) {
 }
 
 func searchIssueInProject(t *testing.T) {
-	int64Pointer := func(i int64) *int64 {
-		return &i
-	}
 	tests := []struct {
 		opts        SearchOptions
 		expectedIDs []int64
 	}{
 		{
 			SearchOptions{
-				ProjectID: int64Pointer(1),
+				ProjectID: optional.Some(int64(1)),
 			},
 			[]int64{5, 3, 2, 1},
 		},
 		{
 			SearchOptions{
-				ProjectBoardID: int64Pointer(1),
+				ProjectBoardID: optional.Some(int64(1)),
 			},
 			[]int64{1},
 		},
 		{
 			SearchOptions{
-				ProjectBoardID: int64Pointer(0), // issue with in default board
+				ProjectBoardID: optional.Some(int64(0)), // issue with in default board
 			},
 			[]int64{2},
 		},
@@ -410,8 +401,8 @@ func searchIssueWithPaginator(t *testing.T) {
 					PageSize: 5,
 				},
 			},
-			[]int64{17, 16, 15, 14, 13},
-			20,
+			[]int64{22, 21, 17, 16, 15},
+			22,
 		},
 	}
 	for _, test := range tests {
diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go
index 031745dd2f..e9c4eca559 100644
--- a/modules/indexer/issues/internal/model.go
+++ b/modules/indexer/issues/internal/model.go
@@ -5,8 +5,8 @@ package internal
 
 import (
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 )
 
 // IndexerData data stored in the issue indexer
@@ -74,11 +74,13 @@ type SearchResult struct {
 type SearchOptions struct {
 	Keyword string // keyword to search
 
+	IsFuzzyKeyword bool // if false the levenshtein distance is 0
+
 	RepoIDs   []int64 // repository IDs which the issues belong to
 	AllPublic bool    // if include all public repositories
 
-	IsPull   util.OptionalBool // if the issues is a pull request
-	IsClosed util.OptionalBool // if the issues is closed
+	IsPull   optional.Option[bool] // if the issues is a pull request
+	IsClosed optional.Option[bool] // if the issues is closed
 
 	IncludedLabelIDs    []int64 // labels the issues have
 	ExcludedLabelIDs    []int64 // labels the issues don't have
@@ -87,24 +89,24 @@ type SearchOptions struct {
 
 	MilestoneIDs []int64 // milestones the issues have
 
-	ProjectID      *int64 // project the issues belong to
-	ProjectBoardID *int64 // project board the issues belong to
+	ProjectID      optional.Option[int64] // project the issues belong to
+	ProjectBoardID optional.Option[int64] // project board the issues belong to
 
-	PosterID *int64 // poster of the issues
+	PosterID optional.Option[int64] // poster of the issues
 
-	AssigneeID *int64 // assignee of the issues, zero means no assignee
+	AssigneeID optional.Option[int64] // assignee of the issues, zero means no assignee
 
-	MentionID *int64 // mentioned user of the issues
+	MentionID optional.Option[int64] // mentioned user of the issues
 
-	ReviewedID        *int64 // reviewer of the issues
-	ReviewRequestedID *int64 // requested reviewer of the issues
+	ReviewedID        optional.Option[int64] // reviewer of the issues
+	ReviewRequestedID optional.Option[int64] // requested reviewer of the issues
 
-	SubscriberID *int64 // subscriber of the issues
+	SubscriberID optional.Option[int64] // subscriber of the issues
 
-	UpdatedAfterUnix  *int64
-	UpdatedBeforeUnix *int64
+	UpdatedAfterUnix  optional.Option[int64]
+	UpdatedBeforeUnix optional.Option[int64]
 
-	db.Paginator
+	Paginator *db.ListOptions
 
 	SortBy SortBy // sort by field
 }
diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go
index 06fddeb65b..7f32876d80 100644
--- a/modules/indexer/issues/internal/tests/tests.go
+++ b/modules/indexer/issues/internal/tests/tests.go
@@ -16,8 +16,8 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/indexer/issues/internal"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -77,6 +77,13 @@ func TestIndexer(t *testing.T, indexer internal.Indexer) {
 				assert.Equal(t, c.ExpectedIDs, ids)
 				assert.Equal(t, c.ExpectedTotal, result.Total)
 			}
+
+			// test counting
+			c.SearchOptions.Paginator = &db.ListOptions{PageSize: 0}
+			countResult, err := indexer.Search(context.Background(), c.SearchOptions)
+			require.NoError(t, err)
+			assert.Empty(t, countResult.Hits)
+			assert.Equal(t, result.Total, countResult.Total)
 		})
 	}
 }
@@ -166,7 +173,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			IsPull: util.OptionalBoolFalse,
+			IsPull: optional.Some(false),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -182,7 +189,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			IsPull: util.OptionalBoolTrue,
+			IsPull: optional.Some(true),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -198,7 +205,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			IsClosed: util.OptionalBoolFalse,
+			IsClosed: optional.Some(false),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -214,7 +221,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			IsClosed: util.OptionalBoolTrue,
+			IsClosed: optional.Some(true),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -300,10 +307,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ProjectID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			ProjectID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -321,10 +325,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ProjectID: func() *int64 {
-				id := int64(0)
-				return &id
-			}(),
+			ProjectID: optional.Some(int64(0)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -342,10 +343,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ProjectBoardID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			ProjectBoardID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -363,10 +361,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ProjectBoardID: func() *int64 {
-				id := int64(0)
-				return &id
-			}(),
+			ProjectBoardID: optional.Some(int64(0)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -384,10 +379,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			PosterID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			PosterID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -405,10 +397,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			AssigneeID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			AssigneeID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -426,10 +415,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			AssigneeID: func() *int64 {
-				id := int64(0)
-				return &id
-			}(),
+			AssigneeID: optional.Some(int64(0)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -447,10 +433,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			MentionID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			MentionID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -468,10 +451,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ReviewedID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			ReviewedID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -489,10 +469,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ReviewRequestedID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			ReviewRequestedID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -510,10 +487,7 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			SubscriberID: func() *int64 {
-				id := int64(1)
-				return &id
-			}(),
+			SubscriberID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -531,14 +505,8 @@ var cases = []*testIndexerCase{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			UpdatedAfterUnix: func() *int64 {
-				var t int64 = 20
-				return &t
-			}(),
-			UpdatedBeforeUnix: func() *int64 {
-				var t int64 = 30
-				return &t
-			}(),
+			UpdatedAfterUnix:  optional.Some(int64(20)),
+			UpdatedBeforeUnix: optional.Some(int64(30)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
@@ -554,10 +522,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByCreatedDesc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByCreatedDesc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByCreatedDesc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -572,10 +538,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByUpdatedDesc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByUpdatedDesc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByUpdatedDesc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -590,10 +554,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByCommentsDesc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByCommentsDesc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByCommentsDesc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -608,10 +570,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByDeadlineDesc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByDeadlineDesc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByDeadlineDesc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -626,10 +586,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByCreatedAsc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByCreatedAsc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByCreatedAsc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -644,10 +602,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByUpdatedAsc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByUpdatedAsc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByUpdatedAsc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -662,10 +618,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByCommentsAsc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByCommentsAsc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByCommentsAsc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
@@ -680,10 +634,8 @@ var cases = []*testIndexerCase{
 	{
 		Name: "SortByDeadlineAsc",
 		SearchOptions: &internal.SearchOptions{
-			Paginator: &db.ListOptions{
-				ListAll: true,
-			},
-			SortBy: internal.SortByDeadlineAsc,
+			Paginator: &db.ListOptionsAll,
+			SortBy:    internal.SortByDeadlineAsc,
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, len(data), len(result.Hits))
diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go
index ab8dcd0af4..8a7cec6cba 100644
--- a/modules/indexer/issues/meilisearch/meilisearch.go
+++ b/modules/indexer/issues/meilisearch/meilisearch.go
@@ -5,6 +5,8 @@ package meilisearch
 
 import (
 	"context"
+	"errors"
+	"fmt"
 	"strconv"
 	"strings"
 
@@ -16,12 +18,15 @@ import (
 )
 
 const (
-	issueIndexerLatestVersion = 2
+	issueIndexerLatestVersion = 3
 
 	// TODO: make this configurable if necessary
 	maxTotalHits = 10000
 )
 
+// ErrMalformedResponse is never expected as we initialize the indexer ourself and so define the types.
+var ErrMalformedResponse = errors.New("meilisearch returned unexpected malformed content")
+
 var _ internal.Indexer = &Indexer{}
 
 // Indexer implements Indexer interface
@@ -47,6 +52,9 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer {
 		},
 		DisplayedAttributes: []string{
 			"id",
+			"title",
+			"content",
+			"comments",
 		},
 		FilterableAttributes: []string{
 			"repo_id",
@@ -131,11 +139,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		query.And(q)
 	}
 
-	if !options.IsPull.IsNone() {
-		query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.IsTrue()))
+	if options.IsPull.Has() {
+		query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.Value()))
 	}
-	if !options.IsClosed.IsNone() {
-		query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.IsTrue()))
+	if options.IsClosed.Has() {
+		query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.Value()))
 	}
 
 	if options.NoLabelOnly {
@@ -163,41 +171,41 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		query.And(inner_meilisearch.NewFilterIn("milestone_id", options.MilestoneIDs...))
 	}
 
-	if options.ProjectID != nil {
-		query.And(inner_meilisearch.NewFilterEq("project_id", *options.ProjectID))
+	if options.ProjectID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value()))
 	}
-	if options.ProjectBoardID != nil {
-		query.And(inner_meilisearch.NewFilterEq("project_board_id", *options.ProjectBoardID))
+	if options.ProjectBoardID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectBoardID.Value()))
 	}
 
-	if options.PosterID != nil {
-		query.And(inner_meilisearch.NewFilterEq("poster_id", *options.PosterID))
+	if options.PosterID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("poster_id", options.PosterID.Value()))
 	}
 
-	if options.AssigneeID != nil {
-		query.And(inner_meilisearch.NewFilterEq("assignee_id", *options.AssigneeID))
+	if options.AssigneeID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("assignee_id", options.AssigneeID.Value()))
 	}
 
-	if options.MentionID != nil {
-		query.And(inner_meilisearch.NewFilterEq("mention_ids", *options.MentionID))
+	if options.MentionID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("mention_ids", options.MentionID.Value()))
 	}
 
-	if options.ReviewedID != nil {
-		query.And(inner_meilisearch.NewFilterEq("reviewed_ids", *options.ReviewedID))
+	if options.ReviewedID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("reviewed_ids", options.ReviewedID.Value()))
 	}
-	if options.ReviewRequestedID != nil {
-		query.And(inner_meilisearch.NewFilterEq("review_requested_ids", *options.ReviewRequestedID))
+	if options.ReviewRequestedID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("review_requested_ids", options.ReviewRequestedID.Value()))
 	}
 
-	if options.SubscriberID != nil {
-		query.And(inner_meilisearch.NewFilterEq("subscriber_ids", *options.SubscriberID))
+	if options.SubscriberID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("subscriber_ids", options.SubscriberID.Value()))
 	}
 
-	if options.UpdatedAfterUnix != nil {
-		query.And(inner_meilisearch.NewFilterGte("updated_unix", *options.UpdatedAfterUnix))
+	if options.UpdatedAfterUnix.Has() {
+		query.And(inner_meilisearch.NewFilterGte("updated_unix", options.UpdatedAfterUnix.Value()))
 	}
-	if options.UpdatedBeforeUnix != nil {
-		query.And(inner_meilisearch.NewFilterLte("updated_unix", *options.UpdatedBeforeUnix))
+	if options.UpdatedBeforeUnix.Has() {
+		query.And(inner_meilisearch.NewFilterLte("updated_unix", options.UpdatedBeforeUnix.Value()))
 	}
 
 	if options.SortBy == "" {
@@ -210,7 +218,22 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 
 	skip, limit := indexer_internal.ParsePaginator(options.Paginator, maxTotalHits)
 
-	searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(options.Keyword, &meilisearch.SearchRequest{
+	counting := limit == 0
+	if counting {
+		// If set limit to 0, it will be 20 by default, and -1 is not allowed.
+		// See https://www.meilisearch.com/docs/reference/api/search#limit
+		// So set limit to 1 to make the cost as low as possible, then clear the result before returning.
+		limit = 1
+	}
+
+	keyword := options.Keyword
+	if !options.IsFuzzyKeyword {
+		// to make it non fuzzy ("typo tolerance" in meilisearch terms), we have to quote the keyword(s)
+		// https://www.meilisearch.com/docs/reference/api/search#phrase-search
+		keyword = doubleQuoteKeyword(keyword)
+	}
+
+	searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(keyword, &meilisearch.SearchRequest{
 		Filter:           query.Statement(),
 		Limit:            int64(limit),
 		Offset:           int64(skip),
@@ -221,11 +244,13 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 		return nil, err
 	}
 
-	hits := make([]internal.Match, 0, len(searchRes.Hits))
-	for _, hit := range searchRes.Hits {
-		hits = append(hits, internal.Match{
-			ID: int64(hit.(map[string]any)["id"].(float64)),
-		})
+	if counting {
+		searchRes.Hits = nil
+	}
+
+	hits, err := convertHits(searchRes)
+	if err != nil {
+		return nil, err
 	}
 
 	return &internal.SearchResult{
@@ -241,3 +266,36 @@ func parseSortBy(sortBy internal.SortBy) string {
 	}
 	return field + ":asc"
 }
+
+func doubleQuoteKeyword(k string) string {
+	kp := strings.Split(k, " ")
+	parts := 0
+	for i := range kp {
+		part := strings.Trim(kp[i], "\"")
+		if part != "" {
+			kp[parts] = fmt.Sprintf(`"%s"`, part)
+			parts++
+		}
+	}
+	return strings.Join(kp[:parts], " ")
+}
+
+func convertHits(searchRes *meilisearch.SearchResponse) ([]internal.Match, error) {
+	hits := make([]internal.Match, 0, len(searchRes.Hits))
+	for _, hit := range searchRes.Hits {
+		hit, ok := hit.(map[string]any)
+		if !ok {
+			return nil, ErrMalformedResponse
+		}
+
+		issueID, ok := hit["id"].(float64)
+		if !ok {
+			return nil, ErrMalformedResponse
+		}
+
+		hits = append(hits, internal.Match{
+			ID: int64(issueID),
+		})
+	}
+	return hits, nil
+}
diff --git a/modules/indexer/issues/meilisearch/meilisearch_test.go b/modules/indexer/issues/meilisearch/meilisearch_test.go
index 3d7237268e..4666df136a 100644
--- a/modules/indexer/issues/meilisearch/meilisearch_test.go
+++ b/modules/indexer/issues/meilisearch/meilisearch_test.go
@@ -10,7 +10,11 @@ import (
 	"testing"
 	"time"
 
+	"code.gitea.io/gitea/modules/indexer/issues/internal"
 	"code.gitea.io/gitea/modules/indexer/issues/internal/tests"
+
+	"github.com/meilisearch/meilisearch-go"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestMeilisearchIndexer(t *testing.T) {
@@ -48,3 +52,44 @@ func TestMeilisearchIndexer(t *testing.T) {
 
 	tests.TestIndexer(t, indexer)
 }
+
+func TestConvertHits(t *testing.T) {
+	_, err := convertHits(&meilisearch.SearchResponse{
+		Hits: []any{"aa", "bb", "cc", "dd"},
+	})
+	assert.ErrorIs(t, err, ErrMalformedResponse)
+
+	validResponse := &meilisearch.SearchResponse{
+		Hits: []any{
+			map[string]any{
+				"id":       float64(11),
+				"title":    "a title",
+				"content":  "issue body with no match",
+				"comments": []any{"hey whats up?", "I'm currently bowling", "nice"},
+			},
+			map[string]any{
+				"id":       float64(22),
+				"title":    "Bowling as title",
+				"content":  "",
+				"comments": []any{},
+			},
+			map[string]any{
+				"id":       float64(33),
+				"title":    "Bowl-ing as fuzzy match",
+				"content":  "",
+				"comments": []any{},
+			},
+		},
+	}
+	hits, err := convertHits(validResponse)
+	assert.NoError(t, err)
+	assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}, {ID: 33}}, hits)
+}
+
+func TestDoubleQuoteKeyword(t *testing.T) {
+	assert.EqualValues(t, "", doubleQuoteKeyword(""))
+	assert.EqualValues(t, `"a" "b" "c"`, doubleQuoteKeyword("a b c"))
+	assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword("a  d g"))
+	assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword("a  d g"))
+	assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword(`a  "" "d" """g`))
+}
diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go
index 510b4060b2..9861c808dc 100644
--- a/modules/indexer/issues/util.go
+++ b/modules/indexer/issues/util.go
@@ -61,9 +61,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
 	)
 	{
 		reviews, err := issue_model.FindReviews(ctx, issue_model.FindReviewOptions{
-			ListOptions: db.ListOptions{
-				ListAll: true,
-			},
+			ListOptions:  db.ListOptionsAll,
 			IssueID:      issueID,
 			OfficialOnly: false,
 		})
diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go
index 4e813fc91f..3be48b9edc 100644
--- a/modules/issue/template/template.go
+++ b/modules/issue/template/template.go
@@ -122,7 +122,13 @@ func validateRequired(field *api.IssueFormField, idx int) error {
 		// The label is not required for a markdown or checkboxes field
 		return nil
 	}
-	return validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required")
+	if err := validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required"); err != nil {
+		return err
+	}
+	if required, _ := field.Validations["required"].(bool); required && !field.VisibleOnForm() {
+		return newErrorPosition(idx, field.Type).Errorf("can not require a hidden field")
+	}
+	return nil
 }
 
 func validateID(field *api.IssueFormField, idx int, ids container.Set[string]) error {
@@ -172,10 +178,38 @@ func validateOptions(field *api.IssueFormField, idx int) error {
 				return position.Errorf("'label' is required and should be a string")
 			}
 
+			if visibility, ok := opt["visible"]; ok {
+				visibilityList, ok := visibility.([]any)
+				if !ok {
+					return position.Errorf("'visible' should be list")
+				}
+				for _, visibleType := range visibilityList {
+					visibleType, ok := visibleType.(string)
+					if !ok || !(visibleType == "form" || visibleType == "content") {
+						return position.Errorf("'visible' list can only contain strings of 'form' and 'content'")
+					}
+				}
+			}
+
 			if required, ok := opt["required"]; ok {
 				if _, ok := required.(bool); !ok {
 					return position.Errorf("'required' should be a bool")
 				}
+
+				// validate if hidden field is required
+				if visibility, ok := opt["visible"]; ok {
+					visibilityList, _ := visibility.([]any)
+					isVisible := false
+					for _, v := range visibilityList {
+						if vv, _ := v.(string); vv == "form" {
+							isVisible = true
+							break
+						}
+					}
+					if !isVisible {
+						return position.Errorf("can not require a hidden checkbox")
+					}
+				}
 			}
 		}
 	}
@@ -238,7 +272,7 @@ func RenderToMarkdown(template *api.IssueTemplate, values url.Values) string {
 			IssueFormField: field,
 			Values:         values,
 		}
-		if f.ID == "" {
+		if f.ID == "" || !f.VisibleInContent() {
 			continue
 		}
 		f.WriteTo(builder)
@@ -253,11 +287,6 @@ type valuedField struct {
 }
 
 func (f *valuedField) WriteTo(builder *strings.Builder) {
-	if f.Type == api.IssueFormFieldTypeMarkdown {
-		// markdown blocks do not appear in output
-		return
-	}
-
 	// write label
 	if !f.HideLabel() {
 		_, _ = fmt.Fprintf(builder, "### %s\n\n", f.Label())
@@ -269,6 +298,9 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
 	switch f.Type {
 	case api.IssueFormFieldTypeCheckboxes:
 		for _, option := range f.Options() {
+			if !option.VisibleInContent() {
+				continue
+			}
 			checked := " "
 			if option.IsChecked() {
 				checked = "x"
@@ -302,6 +334,10 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
 		} else {
 			_, _ = fmt.Fprintf(builder, "%s\n", value)
 		}
+	case api.IssueFormFieldTypeMarkdown:
+		if value, ok := f.Attributes["value"].(string); ok {
+			_, _ = fmt.Fprintf(builder, "%s\n", value)
+		}
 	}
 	_, _ = fmt.Fprintln(builder)
 }
@@ -314,6 +350,9 @@ func (f *valuedField) Label() string {
 }
 
 func (f *valuedField) HideLabel() bool {
+	if f.Type == api.IssueFormFieldTypeMarkdown {
+		return true
+	}
 	if label, ok := f.Attributes["hide_label"].(bool); ok {
 		return label
 	}
@@ -385,6 +424,22 @@ func (o *valuedOption) IsChecked() bool {
 	return false
 }
 
+func (o *valuedOption) VisibleInContent() bool {
+	if o.field.Type == api.IssueFormFieldTypeCheckboxes {
+		if vs, ok := o.data.(map[string]any); ok {
+			if vl, ok := vs["visible"].([]any); ok {
+				for _, v := range vl {
+					if vv, _ := v.(string); vv == "content" {
+						return true
+					}
+				}
+				return false
+			}
+		}
+	}
+	return true
+}
+
 var minQuotesRegex = regexp.MustCompilePOSIX("^`{3,}")
 
 // minQuotes return 3 or more back-quotes.
diff --git a/modules/issue/template/template_test.go b/modules/issue/template/template_test.go
index 06e6b70d35..e24b962d61 100644
--- a/modules/issue/template/template_test.go
+++ b/modules/issue/template/template_test.go
@@ -10,6 +10,7 @@ import (
 	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -318,6 +319,42 @@ body:
 `,
 			wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool",
 		},
+		{
+			name: "field is required but hidden",
+			content: `
+name: "test"
+about: "this is about"
+body:
+  - type: "input"
+    id: "1"
+    attributes:
+      label: "a"
+    validations:
+      required: true
+    visible: [content]
+`,
+			wantErr: "body[0](input): can not require a hidden field",
+		},
+		{
+			name: "checkboxes is required but hidden",
+			content: `
+name: "test"
+about: "this is about"
+body:
+  - type: checkboxes
+    id: "1"
+    attributes:
+      label: Label of checkboxes
+      description: Description of checkboxes
+      options:
+        - label: Option 1
+          required: false
+        - label: Required and hidden
+          required: true
+          visible: [content]
+`,
+			wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox",
+		},
 		{
 			name: "valid",
 			content: `
@@ -374,8 +411,11 @@ body:
           required: true
         - label: Option 2 of checkboxes
           required: false
-        - label: Option 3 of checkboxes
+        - label: Hidden Option 3 of checkboxes
+          visible: [content]
+        - label: Required but not submitted
           required: true
+          visible: [form]
 `,
 			want: &api.IssueTemplate{
 				Name:   "Name",
@@ -390,6 +430,7 @@ body:
 						Attributes: map[string]any{
 							"value": "Value of the markdown",
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
 					},
 					{
 						Type: "textarea",
@@ -404,6 +445,7 @@ body:
 						Validations: map[string]any{
 							"required": true,
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
 					},
 					{
 						Type: "input",
@@ -419,6 +461,7 @@ body:
 							"is_number": true,
 							"regex":     "[a-zA-Z0-9]+",
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
 					},
 					{
 						Type: "dropdown",
@@ -436,6 +479,7 @@ body:
 						Validations: map[string]any{
 							"required": true,
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
 					},
 					{
 						Type: "checkboxes",
@@ -446,9 +490,11 @@ body:
 							"options": []any{
 								map[string]any{"label": "Option 1 of checkboxes", "required": true},
 								map[string]any{"label": "Option 2 of checkboxes", "required": false},
-								map[string]any{"label": "Option 3 of checkboxes", "required": true},
+								map[string]any{"label": "Hidden Option 3 of checkboxes", "visible": []string{"content"}},
+								map[string]any{"label": "Required but not submitted", "required": true, "visible": []string{"form"}},
 							},
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
 					},
 				},
 				FileName: "test.yaml",
@@ -467,7 +513,12 @@ body:
   - type: markdown
     id: id1
     attributes:
-      value: Value of the markdown
+      value: Value of the markdown shown in form
+  - type: markdown
+    id: id2
+    attributes:
+      value: Value of the markdown shown in created issue
+    visible: [content]
 `,
 			want: &api.IssueTemplate{
 				Name:   "Name",
@@ -480,8 +531,17 @@ body:
 						Type: "markdown",
 						ID:   "id1",
 						Attributes: map[string]any{
-							"value": "Value of the markdown",
+							"value": "Value of the markdown shown in form",
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
+					},
+					{
+						Type: "markdown",
+						ID:   "id2",
+						Attributes: map[string]any{
+							"value": "Value of the markdown shown in created issue",
+						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleContent},
 					},
 				},
 				FileName: "test.yaml",
@@ -515,6 +575,7 @@ body:
 						Attributes: map[string]any{
 							"value": "Value of the markdown",
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
 					},
 				},
 				FileName: "test.yaml",
@@ -548,6 +609,7 @@ body:
 						Attributes: map[string]any{
 							"value": "Value of the markdown",
 						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
 					},
 				},
 				FileName: "test.yaml",
@@ -622,9 +684,14 @@ body:
   - type: markdown
     id: id1
     attributes:
-      value: Value of the markdown
-  - type: textarea
+      value: Value of the markdown shown in form
+  - type: markdown
     id: id2
+    attributes:
+      value: Value of the markdown shown in created issue
+    visible: [content]
+  - type: textarea
+    id: id3
     attributes:
       label: Label of textarea
       description: Description of textarea
@@ -634,7 +701,7 @@ body:
     validations:
       required: true
   - type: input
-    id: id3
+    id: id4
     attributes:
       label: Label of input
       description: Description of input
@@ -646,7 +713,7 @@ body:
       is_number: true
       regex: "[a-zA-Z0-9]+"
   - type: dropdown
-    id: id4
+    id: id5
     attributes:
       label: Label of dropdown
       description: Description of dropdown
@@ -658,7 +725,7 @@ body:
     validations:
       required: true
   - type: checkboxes
-    id: id5
+    id: id6
     attributes:
       label: Label of checkboxes
       description: Description of checkboxes
@@ -669,20 +736,26 @@ body:
           required: false
         - label: Option 3 of checkboxes
           required: true
+          visible: [form]
+        - label: Hidden Option of checkboxes
+          visible: [content]
 `,
 				values: map[string][]string{
-					"form-field-id2":   {"Value of id2"},
 					"form-field-id3":   {"Value of id3"},
-					"form-field-id4":   {"0,1"},
-					"form-field-id5-0": {"on"},
-					"form-field-id5-2": {"on"},
+					"form-field-id4":   {"Value of id4"},
+					"form-field-id5":   {"0,1"},
+					"form-field-id6-0": {"on"},
+					"form-field-id6-2": {"on"},
 				},
 			},
-			want: `### Label of textarea
 
-` + "```bash\nValue of id2\n```" + `
+			want: `Value of the markdown shown in created issue
 
-Value of id3
+### Label of textarea
+
+` + "```bash\nValue of id3\n```" + `
+
+Value of id4
 
 ### Label of dropdown
 
@@ -692,7 +765,7 @@ Option 1 of dropdown, Option 2 of dropdown
 
 - [x] Option 1 of checkboxes
 - [ ] Option 2 of checkboxes
-- [x] Option 3 of checkboxes
+- [ ] Hidden Option of checkboxes
 
 `,
 		},
@@ -704,7 +777,7 @@ Option 1 of dropdown, Option 2 of dropdown
 				t.Fatal(err)
 			}
 			if got := RenderToMarkdown(template, tt.args.values); got != tt.want {
-				t.Errorf("RenderToMarkdown() = %v, want %v", got, tt.want)
+				assert.EqualValues(t, tt.want, got)
 			}
 		})
 	}
diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go
index 8cae8d4c42..0fc13d7ddf 100644
--- a/modules/issue/template/unmarshal.go
+++ b/modules/issue/template/unmarshal.go
@@ -128,9 +128,18 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
 			}
 		}
 		for i, v := range it.Fields {
+			// set default id value
 			if v.ID == "" {
 				v.ID = strconv.Itoa(i)
 			}
+			// set default visibility
+			if v.Visible == nil {
+				v.Visible = []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}
+				// markdown is not submitted by default
+				if v.Type != api.IssueFormFieldTypeMarkdown {
+					v.Visible = append(v.Visible, api.IssueFormFieldVisibleContent)
+				}
+			}
 		}
 	}
 
diff --git a/modules/lfs/content_store.go b/modules/lfs/content_store.go
index daf8c6cfdd..0d9c0c98ac 100644
--- a/modules/lfs/content_store.go
+++ b/modules/lfs/content_store.go
@@ -4,6 +4,7 @@
 package lfs
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"errors"
 	"hash"
@@ -12,8 +13,6 @@ import (
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/storage"
-
-	"github.com/minio/sha256-simd"
 )
 
 var (
diff --git a/modules/lfs/filesystem_client.go b/modules/lfs/filesystem_client.go
index 3503a9effc..71bef5c899 100644
--- a/modules/lfs/filesystem_client.go
+++ b/modules/lfs/filesystem_client.go
@@ -44,7 +44,7 @@ func (c *FilesystemClient) Download(ctx context.Context, objects []Pointer, call
 		if err != nil {
 			return err
 		}
-
+		defer f.Close()
 		if err := callback(p, f, nil); err != nil {
 			return err
 		}
@@ -75,7 +75,7 @@ func (c *FilesystemClient) Upload(ctx context.Context, objects []Pointer, callba
 			if err != nil {
 				return err
 			}
-
+			defer f.Close()
 			_, err = io.Copy(f, content)
 
 			return err
diff --git a/modules/lfs/pointer.go b/modules/lfs/pointer.go
index 3e5bb8f91d..ebde20f826 100644
--- a/modules/lfs/pointer.go
+++ b/modules/lfs/pointer.go
@@ -4,6 +4,7 @@
 package lfs
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"errors"
 	"fmt"
@@ -12,8 +13,6 @@ import (
 	"regexp"
 	"strconv"
 	"strings"
-
-	"github.com/minio/sha256-simd"
 )
 
 const (
diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go
index 7af34a6cbc..1dd26eb8ac 100644
--- a/modules/markup/csv/csv.go
+++ b/modules/markup/csv/csv.go
@@ -6,6 +6,7 @@ package markup
 import (
 	"bufio"
 	"bytes"
+	"fmt"
 	"html"
 	"io"
 	"regexp"
@@ -77,27 +78,65 @@ func writeField(w io.Writer, element, class, field string) error {
 }
 
 // Render implements markup.Renderer
-func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
+func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
 	tmpBlock := bufio.NewWriter(output)
+	maxSize := setting.UI.CSV.MaxFileSize
 
-	// FIXME: don't read all to memory
-	rawBytes, err := io.ReadAll(input)
+	if maxSize == 0 {
+		return r.tableRender(ctx, input, tmpBlock)
+	}
+
+	rawBytes, err := io.ReadAll(io.LimitReader(input, maxSize+1))
 	if err != nil {
 		return err
 	}
 
-	if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < int64(len(rawBytes)) {
-		if _, err := tmpBlock.WriteString("<pre>"); err != nil {
-			return err
-		}
-		if _, err := tmpBlock.WriteString(html.EscapeString(string(rawBytes))); err != nil {
-			return err
-		}
-		_, err = tmpBlock.WriteString("</pre>")
+	if int64(len(rawBytes)) <= maxSize {
+		return r.tableRender(ctx, bytes.NewReader(rawBytes), tmpBlock)
+	}
+	return r.fallbackRender(io.MultiReader(bytes.NewReader(rawBytes), input), tmpBlock)
+}
+
+func (Renderer) fallbackRender(input io.Reader, tmpBlock *bufio.Writer) error {
+	_, err := tmpBlock.WriteString("<pre>")
+	if err != nil {
 		return err
 	}
 
-	rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, bytes.NewReader(rawBytes))
+	scan := bufio.NewScanner(input)
+	scan.Split(bufio.ScanRunes)
+	for scan.Scan() {
+		switch scan.Text() {
+		case `&`:
+			_, err = tmpBlock.WriteString("&amp;")
+		case `'`:
+			_, err = tmpBlock.WriteString("&#39;") // "&#39;" is shorter than "&apos;" and apos was not in HTML until HTML5.
+		case `<`:
+			_, err = tmpBlock.WriteString("&lt;")
+		case `>`:
+			_, err = tmpBlock.WriteString("&gt;")
+		case `"`:
+			_, err = tmpBlock.WriteString("&#34;") // "&#34;" is shorter than "&quot;".
+		default:
+			_, err = tmpBlock.Write(scan.Bytes())
+		}
+		if err != nil {
+			return err
+		}
+	}
+	if err = scan.Err(); err != nil {
+		return fmt.Errorf("fallbackRender scan: %w", err)
+	}
+
+	_, err = tmpBlock.WriteString("</pre>")
+	if err != nil {
+		return err
+	}
+	return tmpBlock.Flush()
+}
+
+func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock *bufio.Writer) error {
+	rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, input)
 	if err != nil {
 		return err
 	}
diff --git a/modules/markup/csv/csv_test.go b/modules/markup/csv/csv_test.go
index 8c07184b21..3d12be477c 100644
--- a/modules/markup/csv/csv_test.go
+++ b/modules/markup/csv/csv_test.go
@@ -4,6 +4,8 @@
 package markup
 
 import (
+	"bufio"
+	"bytes"
 	"strings"
 	"testing"
 
@@ -29,4 +31,12 @@ func TestRenderCSV(t *testing.T) {
 		assert.NoError(t, err)
 		assert.EqualValues(t, v, buf.String())
 	}
+
+	t.Run("fallbackRender", func(t *testing.T) {
+		var buf bytes.Buffer
+		err := render.fallbackRender(strings.NewReader("1,<a>\n2,<b>"), bufio.NewWriter(&buf))
+		assert.NoError(t, err)
+		want := "<pre>1,&lt;a&gt;\n2,&lt;b&gt;</pre>"
+		assert.Equal(t, want, buf.String())
+	})
 }
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 33dc1e9086..cef643bf18 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -53,38 +53,38 @@ var (
 	// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
 	shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
 
-	// anySHA1Pattern splits url containing SHA into parts
+	// anyHashPattern splits url containing SHA into parts
 	anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~_%.a-zA-Z0-9/]+)?(#[-+~_%.a-zA-Z0-9]+)?`)
 
 	// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
 	comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`)
 
-	validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`)
+	// fullURLPattern matches full URL like "mailto:...", "https://..." and "ssh+git://..."
+	fullURLPattern = regexp.MustCompile(`^[a-z][-+\w]+:`)
 
-	// While this email regex is definitely not perfect and I'm sure you can come up
-	// with edge cases, it is still accepted by the CommonMark specification, as
-	// well as the HTML5 spec:
+	// emailRegex is definitely not perfect with edge cases,
+	// it is still accepted by the CommonMark specification, as well as the HTML5 spec:
 	//   http://spec.commonmark.org/0.28/#email-address
 	//   https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
 	emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
 
-	// blackfriday extensions create IDs like fn:user-content-footnote
+	// blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote
 	blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
 
-	// EmojiShortCodeRegex find emoji by alias like :smile:
-	EmojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
+	// emojiShortCodeRegex find emoji by alias like :smile:
+	emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
 )
 
 // CSS class for action keywords (e.g. "closes: #1")
 const keywordClass = "issue-keyword"
 
-// IsLink reports whether link fits valid format.
-func IsLink(link []byte) bool {
-	return validLinksPattern.Match(link)
+// IsFullURLBytes reports whether link fits valid format.
+func IsFullURLBytes(link []byte) bool {
+	return fullURLPattern.Match(link)
 }
 
-func IsLinkStr(link string) bool {
-	return validLinksPattern.MatchString(link)
+func IsFullURLString(link string) bool {
+	return fullURLPattern.MatchString(link)
 }
 
 // regexp for full links to issues/pulls
@@ -171,6 +171,7 @@ type processor func(ctx *RenderContext, node *html.Node)
 var defaultProcessors = []processor{
 	fullIssuePatternProcessor,
 	comparePatternProcessor,
+	codePreviewPatternProcessor,
 	fullHashPatternProcessor,
 	shortLinkProcessor,
 	linkProcessor,
@@ -399,7 +400,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
 				if attr.Key != "src" {
 					continue
 				}
-				if len(attr.Val) > 0 && !IsLinkStr(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") {
+				if len(attr.Val) > 0 && !IsFullURLString(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") {
 					attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
 				}
 				attr.Val = camoHandleLink(attr.Val)
@@ -609,7 +610,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
 		if ok && strings.Contains(mention, "/") {
 			mentionOrgAndTeam := strings.Split(mention, "/")
 			if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
-				replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention"))
+				replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention"))
 				node = node.NextSibling.NextSibling
 				start = 0
 				continue
@@ -620,7 +621,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
 		mentionedUsername := mention[1:]
 
 		if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) {
-			replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mentionedUsername), mention, "mention"))
+			replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), mentionedUsername), mention, "mention"))
 			node = node.NextSibling.NextSibling
 		} else {
 			node = node.NextSibling
@@ -650,7 +651,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
 			if equalPos := strings.IndexByte(v, '='); equalPos == -1 {
 				// There is no equal in this argument; this is a mandatory arg
 				if props["name"] == "" {
-					if IsLinkStr(v) {
+					if IsFullURLString(v) {
 						// If we clearly see it is a link, we save it so
 
 						// But first we need to ensure, that if both mandatory args provided
@@ -708,7 +709,8 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
 
 		name += tail
 		image := false
-		switch ext := filepath.Ext(link); ext {
+		ext := filepath.Ext(link)
+		switch ext {
 		// fast path: empty string, ignore
 		case "":
 			// leave image as false
@@ -725,7 +727,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
 			DataAtom:   atom.A,
 		}
 		childNode.Parent = linkNode
-		absoluteLink := IsLinkStr(link)
+		absoluteLink := IsFullURLString(link)
 		if !absoluteLink {
 			if image {
 				link = strings.ReplaceAll(link, " ", "+")
@@ -766,11 +768,26 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
 			}
 		} else {
 			if !absoluteLink {
+				var base string
 				if ctx.IsWiki {
-					link = util.URLJoin(ctx.Links.WikiLink(), link)
+					switch ext {
+					case "":
+						// no file extension, create a regular wiki link
+						base = ctx.Links.WikiLink()
+					default:
+						// we have a file extension:
+						// return a regular wiki link if it's a renderable file (extension),
+						// raw link otherwise
+						if Type(link) != "" {
+							base = ctx.Links.WikiLink()
+						} else {
+							base = ctx.Links.WikiRawLink()
+						}
+					}
 				} else {
-					link = util.URLJoin(ctx.Links.SrcLink(), link)
+					base = ctx.Links.SrcLink()
 				}
+				link = util.URLJoin(base, link)
 			}
 			childNode.Type = html.TextNode
 			childNode.Data = name
@@ -804,7 +821,7 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
 		// indicate that in the text by appending (comment)
 		if m[4] != -1 && m[5] != -1 {
 			if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
-				text += " " + locale.Tr("repo.from_comment")
+				text += " " + locale.TrString("repo.from_comment")
 			} else {
 				text += " (comment)"
 			}
@@ -898,9 +915,9 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
 				path = "pulls"
 			}
 			if ref.Owner == "" {
-				link = createLink(util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue")
+				link = createLink(util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue")
 			} else {
-				link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue")
+				link = createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue")
 			}
 		}
 
@@ -939,7 +956,7 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
 		}
 
 		reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
-		link := createLink(util.URLJoin(setting.AppSubURL, ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
+		link := createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
 
 		replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
 		node = node.NextSibling.NextSibling
@@ -1059,7 +1076,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
 	start := 0
 	next := node.NextSibling
 	for node != nil && node != next && start < len(node.Data) {
-		m := EmojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:])
+		m := emojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:])
 		if m == nil {
 			return
 		}
@@ -1166,7 +1183,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
 			continue
 		}
 
-		link := util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], "commit", hash)
+		link := util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], "commit", hash)
 		replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
 		start = 0
 		node = node.NextSibling.NextSibling
diff --git a/modules/markup/html_codepreview.go b/modules/markup/html_codepreview.go
new file mode 100644
index 0000000000..d9da24ea34
--- /dev/null
+++ b/modules/markup/html_codepreview.go
@@ -0,0 +1,92 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+	"html/template"
+	"net/url"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"code.gitea.io/gitea/modules/httplib"
+	"code.gitea.io/gitea/modules/log"
+
+	"golang.org/x/net/html"
+)
+
+// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
+var codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
+
+type RenderCodePreviewOptions struct {
+	FullURL   string
+	OwnerName string
+	RepoName  string
+	CommitID  string
+	FilePath  string
+
+	LineStart, LineStop int
+}
+
+func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosStop int, htm template.HTML, err error) {
+	m := codePreviewPattern.FindStringSubmatchIndex(node.Data)
+	if m == nil {
+		return 0, 0, "", nil
+	}
+
+	opts := RenderCodePreviewOptions{
+		FullURL:   node.Data[m[0]:m[1]],
+		OwnerName: node.Data[m[2]:m[3]],
+		RepoName:  node.Data[m[4]:m[5]],
+		CommitID:  node.Data[m[6]:m[7]],
+		FilePath:  node.Data[m[8]:m[9]],
+	}
+	if !httplib.IsCurrentGiteaSiteURL(opts.FullURL) {
+		return 0, 0, "", nil
+	}
+	u, err := url.Parse(opts.FilePath)
+	if err != nil {
+		return 0, 0, "", err
+	}
+	opts.FilePath = strings.TrimPrefix(u.Path, "/")
+
+	lineStartStr, lineStopStr, _ := strings.Cut(node.Data[m[10]:m[11]], "-")
+	lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L"))
+	lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L"))
+	opts.LineStart, opts.LineStop = lineStart, lineStop
+	h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx.Ctx, opts)
+	return m[0], m[1], h, err
+}
+
+func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
+	for node != nil {
+		if node.Type != html.TextNode {
+			node = node.NextSibling
+			continue
+		}
+		urlPosStart, urlPosEnd, h, err := renderCodeBlock(ctx, node)
+		if err != nil || h == "" {
+			if err != nil {
+				log.Error("Unable to render code preview: %v", err)
+			}
+			node = node.NextSibling
+			continue
+		}
+		next := node.NextSibling
+		textBefore := node.Data[:urlPosStart]
+		textAfter := node.Data[urlPosEnd:]
+		// "textBefore" could be empty if there is only a URL in the text node, then an empty node (p, or li) will be left here.
+		// However, the empty node can't be simply removed, because:
+		// 1. the following processors will still try to access it (need to double-check undefined behaviors)
+		// 2. the new node is inserted as "<p>{TextBefore}<div NewNode/>{TextAfter}</p>" (the parent could also be "li")
+		//    then it is resolved as: "<p>{TextBefore}</p><div NewNode/><p>{TextAfter}</p>",
+		//    so unless it could correctly replace the parent "p/li" node, it is very difficult to eliminate the "TextBefore" empty node.
+		node.Data = textBefore
+		node.Parent.InsertBefore(&html.Node{Type: html.RawNode, Data: string(h)}, next)
+		if textAfter != "" {
+			node.Parent.InsertBefore(&html.Node{Type: html.TextNode, Data: textAfter}, next)
+		}
+		node = next
+	}
+}
diff --git a/modules/markup/html_codepreview_test.go b/modules/markup/html_codepreview_test.go
new file mode 100644
index 0000000000..d33630d040
--- /dev/null
+++ b/modules/markup/html_codepreview_test.go
@@ -0,0 +1,34 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup_test
+
+import (
+	"context"
+	"html/template"
+	"strings"
+	"testing"
+
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/markup"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRenderCodePreview(t *testing.T) {
+	markup.Init(&markup.ProcessorHelper{
+		RenderRepoFileCodePreview: func(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
+			return "<div>code preview</div>", nil
+		},
+	})
+	test := func(input, expected string) {
+		buffer, err := markup.RenderString(&markup.RenderContext{
+			Ctx:  git.DefaultContext,
+			Type: "markdown",
+		}, input)
+		assert.NoError(t, err)
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+	}
+	test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "<p><div>code preview</div></p>")
+	test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`)
+}
diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go
index 93ba9d7667..e313be7040 100644
--- a/modules/markup/html_internal_test.go
+++ b/modules/markup/html_internal_test.go
@@ -287,6 +287,7 @@ func TestRender_IssueIndexPattern_Document(t *testing.T) {
 }
 
 func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
+	ctx.Links.AbsolutePrefix = true
 	if ctx.Links.Base == "" {
 		ctx.Links.Base = TestRepoURL
 	}
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 89ecfc036b..916e74fb62 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -43,7 +43,8 @@ func TestRender_Commits(t *testing.T) {
 			Ctx:          git.DefaultContext,
 			RelativePath: ".md",
 			Links: markup.Links{
-				Base: markup.TestRepoURL,
+				AbsolutePrefix: true,
+				Base:           markup.TestRepoURL,
 			},
 			Metas: localMetas,
 		}, input)
@@ -96,7 +97,8 @@ func TestRender_CrossReferences(t *testing.T) {
 			Ctx:          git.DefaultContext,
 			RelativePath: "a.md",
 			Links: markup.Links{
-				Base: setting.AppSubURL,
+				AbsolutePrefix: true,
+				Base:           setting.AppSubURL,
 			},
 			Metas: localMetas,
 		}, input)
@@ -204,6 +206,15 @@ func TestRender_links(t *testing.T) {
 	test(
 		"magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download",
 		`<p><a href="magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&amp;dn=download" rel="nofollow">magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&amp;dn=download</a></p>`)
+	test(
+		`[link](https://example.com)`,
+		`<p><a href="https://example.com" rel="nofollow">link</a></p>`)
+	test(
+		`[link](mailto:test@example.com)`,
+		`<p><a href="mailto:test@example.com" rel="nofollow">link</a></p>`)
+	test(
+		`[link](javascript:xss)`,
+		`<p>link</p>`)
 
 	// Test that should *not* be turned into URL
 	test(
@@ -388,7 +399,7 @@ func TestRender_ShortLinks(t *testing.T) {
 			},
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 		buffer, err = markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
@@ -398,7 +409,7 @@ func TestRender_ShortLinks(t *testing.T) {
 			IsWiki: true,
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
 	}
 
 	mediatree := util.URLJoin(markup.TestRepoURL, "media", "master")
@@ -416,6 +427,10 @@ func TestRender_ShortLinks(t *testing.T) {
 	otherImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link+Other.jpg")
 	encodedImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link+%23.jpg")
 	notencodedImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "some", "path", "Link+#.jpg")
+	renderableFileURL := util.URLJoin(tree, "markdown_file.md")
+	renderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "markdown_file.md")
+	unrenderableFileURL := util.URLJoin(tree, "file.zip")
+	unrenderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "file.zip")
 	favicon := "http://google.com/favicon.ico"
 
 	test(
@@ -470,6 +485,14 @@ func TestRender_ShortLinks(t *testing.T) {
 		"[[Link]] [[Other Link]] [[Link?]]",
 		`<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherURL+`" rel="nofollow">Other Link</a> <a href="`+encodedURL+`" rel="nofollow">Link?</a></p>`,
 		`<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherURLWiki+`" rel="nofollow">Other Link</a> <a href="`+encodedURLWiki+`" rel="nofollow">Link?</a></p>`)
+	test(
+		"[[markdown_file.md]]",
+		`<p><a href="`+renderableFileURL+`" rel="nofollow">markdown_file.md</a></p>`,
+		`<p><a href="`+renderableFileURLWiki+`" rel="nofollow">markdown_file.md</a></p>`)
+	test(
+		"[[file.zip]]",
+		`<p><a href="`+unrenderableFileURL+`" rel="nofollow">file.zip</a></p>`,
+		`<p><a href="`+unrenderableFileURLWiki+`" rel="nofollow">file.zip</a></p>`)
 	test(
 		"[[Link #.jpg]]",
 		`<p><a href="`+encodedImgurl+`" rel="nofollow"><img src="`+encodedImgurl+`" title="Link #.jpg" alt="Link #.jpg"/></a></p>`,
@@ -501,7 +524,7 @@ func TestRender_RelativeImages(t *testing.T) {
 			Metas: localMetas,
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 		buffer, err = markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
@@ -511,7 +534,7 @@ func TestRender_RelativeImages(t *testing.T) {
 			IsWiki: true,
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
 	}
 
 	rawwiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw")
@@ -579,7 +602,8 @@ func TestPostProcess_RenderDocument(t *testing.T) {
 		err := markup.PostProcess(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: "https://example.com",
+				AbsolutePrefix: true,
+				Base:           "https://example.com",
 			},
 			Metas: localMetas,
 		}, strings.NewReader(input), &res)
@@ -673,3 +697,9 @@ func TestIssue18471(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, "<a href=\"http://domain/org/repo/compare/783b039...da951ce\" class=\"compare\"><code class=\"nohighlight\">783b039...da951ce</code></a>", res.String())
 }
+
+func TestIsFullURL(t *testing.T) {
+	assert.True(t, markup.IsFullURLString("https://example.com"))
+	assert.True(t, markup.IsFullURLString("mailto:test@example.com"))
+	assert.False(t, markup.IsFullURLString("/foo:bar"))
+}
diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go
index 3e6e291ab2..624c35d945 100644
--- a/modules/markup/markdown/ast.go
+++ b/modules/markup/markdown/ast.go
@@ -175,19 +175,7 @@ func NewColorPreview(color []byte) *ColorPreview {
 	}
 }
 
-// IsColorPreview returns true if the given node implements the ColorPreview interface,
-// otherwise false.
-func IsColorPreview(node ast.Node) bool {
-	_, ok := node.(*ColorPreview)
-	return ok
-}
-
-const (
-	AttentionNote    string = "Note"
-	AttentionWarning string = "Warning"
-)
-
-// Attention is an inline for a color preview
+// Attention is an inline for an attention
 type Attention struct {
 	ast.BaseInline
 	AttentionType string
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index 178e3d2fdd..b8b3aeaab0 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -4,19 +4,14 @@
 package markdown
 
 import (
-	"bytes"
 	"fmt"
 	"regexp"
 	"strings"
 
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/markup"
-	"code.gitea.io/gitea/modules/markup/common"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/svg"
-	giteautil "code.gitea.io/gitea/modules/util"
 
-	"github.com/microcosm-cc/bluemonday/css"
 	"github.com/yuin/goldmark/ast"
 	east "github.com/yuin/goldmark/extension/ast"
 	"github.com/yuin/goldmark/parser"
@@ -26,10 +21,22 @@ import (
 	"github.com/yuin/goldmark/util"
 )
 
-var byteMailto = []byte("mailto:")
-
 // ASTTransformer is a default transformer of the goldmark tree.
-type ASTTransformer struct{}
+type ASTTransformer struct {
+	attentionTypes container.Set[string]
+}
+
+func NewASTTransformer() *ASTTransformer {
+	return &ASTTransformer{
+		attentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
+	}
+}
+
+func (g *ASTTransformer) applyElementDir(n ast.Node) {
+	if markup.DefaultProcessorHelper.ElementDir != "" {
+		n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
+	}
+}
 
 // Transform transforms the given AST tree.
 func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
@@ -47,13 +54,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 		tocMode = rc.TOC
 	}
 
-	applyElementDir := func(n ast.Node) {
-		if markup.DefaultProcessorHelper.ElementDir != "" {
-			n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
-		}
-	}
-
-	attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote])
 	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
 		if !entering {
 			return ast.WalkContinue, nil
@@ -61,129 +61,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 
 		switch v := n.(type) {
 		case *ast.Heading:
-			for _, attr := range v.Attributes() {
-				if _, ok := attr.Value.([]byte); !ok {
-					v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
-				}
-			}
-			txt := n.Text(reader.Source())
-			header := markup.Header{
-				Text:  util.BytesToReadOnlyString(txt),
-				Level: v.Level,
-			}
-			if id, found := v.AttributeString("id"); found {
-				header.ID = util.BytesToReadOnlyString(id.([]byte))
-			}
-			tocList = append(tocList, header)
-			applyElementDir(v)
+			g.transformHeading(ctx, v, reader, &tocList)
 		case *ast.Paragraph:
-			applyElementDir(v)
+			g.applyElementDir(v)
 		case *ast.Image:
-			// Images need two things:
-			//
-			// 1. Their src needs to munged to be a real value
-			// 2. If they're not wrapped with a link they need a link wrapper
-
-			// Check if the destination is a real link
-			if len(v.Destination) > 0 && !markup.IsLink(v.Destination) {
-				v.Destination = []byte(giteautil.URLJoin(
-					ctx.Links.ResolveMediaLink(ctx.IsWiki),
-					strings.TrimLeft(string(v.Destination), "/"),
-				))
-			}
-
-			parent := n.Parent()
-			// Create a link around image only if parent is not already a link
-			if _, ok := parent.(*ast.Link); !ok && parent != nil {
-				next := n.NextSibling()
-
-				// Create a link wrapper
-				wrap := ast.NewLink()
-				wrap.Destination = v.Destination
-				wrap.Title = v.Title
-				wrap.SetAttributeString("target", []byte("_blank"))
-
-				// Duplicate the current image node
-				image := ast.NewImage(ast.NewLink())
-				image.Destination = v.Destination
-				image.Title = v.Title
-				for _, attr := range v.Attributes() {
-					image.SetAttribute(attr.Name, attr.Value)
-				}
-				for child := v.FirstChild(); child != nil; {
-					next := child.NextSibling()
-					image.AppendChild(image, child)
-					child = next
-				}
-
-				// Append our duplicate image to the wrapper link
-				wrap.AppendChild(wrap, image)
-
-				// Wire in the next sibling
-				wrap.SetNextSibling(next)
-
-				// Replace the current node with the wrapper link
-				parent.ReplaceChild(parent, n, wrap)
-
-				// But most importantly ensure the next sibling is still on the old image too
-				v.SetNextSibling(next)
-			}
+			g.transformImage(ctx, v, reader)
 		case *ast.Link:
-			// Links need their href to munged to be a real value
-			link := v.Destination
-			if len(link) > 0 && !markup.IsLink(link) &&
-				link[0] != '#' && !bytes.HasPrefix(link, byteMailto) {
-				// special case: this is not a link, a hash link or a mailto:, so it's a
-				// relative URL
-
-				var base string
-				if ctx.IsWiki {
-					base = ctx.Links.WikiLink()
-				} else if ctx.Links.HasBranchInfo() {
-					base = ctx.Links.SrcLink()
-				} else {
-					base = ctx.Links.Base
-				}
-
-				link = []byte(giteautil.URLJoin(base, string(link)))
-			}
-			if len(link) > 0 && link[0] == '#' {
-				link = []byte("#user-content-" + string(link)[1:])
-			}
-			v.Destination = link
+			g.transformLink(ctx, v, reader)
 		case *ast.List:
-			if v.HasChildren() {
-				children := make([]ast.Node, 0, v.ChildCount())
-				child := v.FirstChild()
-				for child != nil {
-					children = append(children, child)
-					child = child.NextSibling()
-				}
-				v.RemoveChildren(v)
-
-				for _, child := range children {
-					listItem := child.(*ast.ListItem)
-					if !child.HasChildren() || !child.FirstChild().HasChildren() {
-						v.AppendChild(v, child)
-						continue
-					}
-					taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
-					if !ok {
-						v.AppendChild(v, child)
-						continue
-					}
-					newChild := NewTaskCheckBoxListItem(listItem)
-					newChild.IsChecked = taskCheckBox.IsChecked
-					newChild.SetAttributeString("class", []byte("task-list-item"))
-					segments := newChild.FirstChild().Lines()
-					if segments.Len() > 0 {
-						segment := segments.At(0)
-						newChild.SourcePosition = rc.metaLength + segment.Start
-					}
-					v.AppendChild(v, newChild)
-				}
-			}
-			applyElementDir(v)
+			g.transformList(ctx, v, reader, rc)
 		case *ast.Text:
 			if v.SoftLineBreak() && !v.HardLineBreak() {
 				if ctx.Metas["mode"] != "document" {
@@ -193,22 +79,9 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 				}
 			}
 		case *ast.CodeSpan:
-			colorContent := n.Text(reader.Source())
-			if css.ColorHandler(strings.ToLower(string(colorContent))) {
-				v.AppendChild(v, NewColorPreview(colorContent))
-			}
-		case *ast.Emphasis:
-			// check if inside blockquote for attention, expected hierarchy is
-			// Emphasis < Paragraph < Blockquote
-			blockquote, isInBlockquote := n.Parent().Parent().(*ast.Blockquote)
-			if isInBlockquote && !attentionMarkedBlockquotes.Contains(blockquote) {
-				fullText := string(n.Text(reader.Source()))
-				if fullText == AttentionNote || fullText == AttentionWarning {
-					v.SetAttributeString("class", []byte("attention-"+strings.ToLower(fullText)))
-					v.Parent().InsertBefore(v.Parent(), v, NewAttention(fullText))
-					attentionMarkedBlockquotes.Add(blockquote)
-				}
-			}
+			g.transformCodeSpan(ctx, v, reader)
+		case *ast.Blockquote:
+			return g.transformBlockquote(v, reader)
 		}
 		return ast.WalkContinue, nil
 	})
@@ -230,55 +103,12 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 	}
 }
 
-type prefixedIDs struct {
-	values container.Set[string]
-}
-
-// Generate generates a new element id.
-func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
-	dft := []byte("id")
-	if kind == ast.KindHeading {
-		dft = []byte("heading")
-	}
-	return p.GenerateWithDefault(value, dft)
-}
-
-// Generate generates a new element id.
-func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
-	result := common.CleanValue(value)
-	if len(result) == 0 {
-		result = dft
-	}
-	if !bytes.HasPrefix(result, []byte("user-content-")) {
-		result = append([]byte("user-content-"), result...)
-	}
-	if p.values.Add(util.BytesToReadOnlyString(result)) {
-		return result
-	}
-	for i := 1; ; i++ {
-		newResult := fmt.Sprintf("%s-%d", result, i)
-		if p.values.Add(newResult) {
-			return []byte(newResult)
-		}
-	}
-}
-
-// Put puts a given element id to the used ids table.
-func (p *prefixedIDs) Put(value []byte) {
-	p.values.Add(util.BytesToReadOnlyString(value))
-}
-
-func newPrefixedIDs() *prefixedIDs {
-	return &prefixedIDs{
-		values: make(container.Set[string]),
-	}
-}
-
 // NewHTMLRenderer creates a HTMLRenderer to render
 // in the gitea form.
 func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
 	r := &HTMLRenderer{
-		Config: html.NewConfig(),
+		Config:      html.NewConfig(),
+		reValidName: regexp.MustCompile("^[a-z ]+$"),
 	}
 	for _, opt := range opts {
 		opt.SetHTMLOption(&r.Config)
@@ -290,6 +120,7 @@ func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
 // renders gitea specific features.
 type HTMLRenderer struct {
 	html.Config
+	reValidName *regexp.Regexp
 }
 
 // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
@@ -304,60 +135,6 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
 	reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
 }
 
-// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
-// See #21474 for reference
-func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
-	if entering {
-		if n.Attributes() != nil {
-			_, _ = w.WriteString("<code")
-			html.RenderAttributes(w, n, html.CodeAttributeFilter)
-			_ = w.WriteByte('>')
-		} else {
-			_, _ = w.WriteString("<code>")
-		}
-		for c := n.FirstChild(); c != nil; c = c.NextSibling() {
-			switch v := c.(type) {
-			case *ast.Text:
-				segment := v.Segment
-				value := segment.Value(source)
-				if bytes.HasSuffix(value, []byte("\n")) {
-					r.Writer.RawWrite(w, value[:len(value)-1])
-					r.Writer.RawWrite(w, []byte(" "))
-				} else {
-					r.Writer.RawWrite(w, value)
-				}
-			case *ColorPreview:
-				_, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
-			}
-		}
-		return ast.WalkSkipChildren, nil
-	}
-	_, _ = w.WriteString("</code>")
-	return ast.WalkContinue, nil
-}
-
-// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
-func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	if entering {
-		_, _ = w.WriteString(`<span class="attention-icon attention-`)
-		n := node.(*Attention)
-		_, _ = w.WriteString(strings.ToLower(n.AttentionType))
-		_, _ = w.WriteString(`">`)
-
-		var octiconType string
-		switch n.AttentionType {
-		case AttentionNote:
-			octiconType = "info"
-		case AttentionWarning:
-			octiconType = "alert"
-		}
-		_, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType)))
-	} else {
-		_, _ = w.WriteString("</span>\n")
-	}
-	return ast.WalkContinue, nil
-}
-
 func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 	n := node.(*ast.Document)
 
@@ -417,8 +194,6 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
 	return ast.WalkContinue, nil
 }
 
-var validNameRE = regexp.MustCompile("^[a-z ]+$")
-
 func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 	if !entering {
 		return ast.WalkContinue, nil
@@ -433,7 +208,7 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
 		return ast.WalkContinue, nil
 	}
 
-	if !validNameRE.MatchString(name) {
+	if !r.reValidName.MatchString(name) {
 		// skip this
 		return ast.WalkContinue, nil
 	}
@@ -446,38 +221,3 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
 
 	return ast.WalkContinue, nil
 }
-
-func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	n := node.(*TaskCheckBoxListItem)
-	if entering {
-		if n.Attributes() != nil {
-			_, _ = w.WriteString("<li")
-			html.RenderAttributes(w, n, html.ListItemAttributeFilter)
-			_ = w.WriteByte('>')
-		} else {
-			_, _ = w.WriteString("<li>")
-		}
-		fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition)
-		if n.IsChecked {
-			_, _ = w.WriteString(` checked=""`)
-		}
-		if r.XHTML {
-			_, _ = w.WriteString(` />`)
-		} else {
-			_ = w.WriteByte('>')
-		}
-		fc := n.FirstChild()
-		if fc != nil {
-			if _, ok := fc.(*ast.TextBlock); !ok {
-				_ = w.WriteByte('\n')
-			}
-		}
-	} else {
-		_, _ = w.WriteString("</li>\n")
-	}
-	return ast.WalkContinue, nil
-}
-
-func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	return ast.WalkContinue, nil
-}
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index 771162b9a3..db4e5706f6 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -6,6 +6,7 @@ package markdown
 
 import (
 	"fmt"
+	"html/template"
 	"io"
 	"strings"
 	"sync"
@@ -103,7 +104,8 @@ func SpecializedMarkdown() goldmark.Markdown {
 							}
 
 							// include language-x class as part of commonmark spec
-							_, err = w.WriteString(`<code class="chroma language-` + string(language) + `">`)
+							// the "display" class is used by "js/markup/math.js" to render the code element as a block
+							_, err = w.WriteString(`<code class="chroma language-` + string(language) + ` display">`)
 							if err != nil {
 								return
 							}
@@ -124,7 +126,7 @@ func SpecializedMarkdown() goldmark.Markdown {
 				parser.WithAttribute(),
 				parser.WithAutoHeadingID(),
 				parser.WithASTTransformers(
-					util.Prioritized(&ASTTransformer{}, 10000),
+					util.Prioritized(NewASTTransformer(), 10000),
 				),
 			),
 			goldmark.WithRendererOptions(
@@ -262,12 +264,12 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
 }
 
 // RenderString renders Markdown string to HTML with all specific handling stuff and return string
-func RenderString(ctx *markup.RenderContext, content string) (string, error) {
+func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) {
 	var buf strings.Builder
 	if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
 		return "", err
 	}
-	return buf.String(), nil
+	return template.HTML(buf.String()), nil
 }
 
 // RenderRaw renders Markdown to HTML without handling special links.
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index bdf4011fa2..d9b67e43af 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -5,6 +5,7 @@ package markdown_test
 
 import (
 	"context"
+	"html/template"
 	"os"
 	"strings"
 	"testing"
@@ -15,18 +16,20 @@ import (
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/svg"
 	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
+	"golang.org/x/text/cases"
+	"golang.org/x/text/language"
 )
 
 const (
-	AppURL    = "http://localhost:3000/"
-	Repo      = "gogits/gogs"
-	AppSubURL = AppURL + Repo + "/"
+	AppURL  = "http://localhost:3000/"
+	FullURL = AppURL + "gogits/gogs/"
 )
 
-// these values should match the Repo const above
+// these values should match the const above
 var localMetas = map[string]string{
 	"user":     "gogits",
 	"repo":     "gogs",
@@ -48,34 +51,33 @@ func TestMain(m *testing.M) {
 
 func TestRender_StandardLinks(t *testing.T) {
 	setting.AppURL = AppURL
-	setting.AppSubURL = AppSubURL
 
 	test := func(input, expected, expectedWiki string) {
 		buffer, err := markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: setting.AppSubURL,
+				Base: FullURL,
 			},
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 
 		buffer, err = markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: setting.AppSubURL,
+				Base: FullURL,
 			},
 			IsWiki: true,
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
 	}
 
 	googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>`
 	test("<https://google.com/>", googleRendered, googleRendered)
 
-	lnk := util.URLJoin(AppSubURL, "WikiPage")
-	lnkWiki := util.URLJoin(AppSubURL, "wiki", "WikiPage")
+	lnk := util.URLJoin(FullURL, "WikiPage")
+	lnkWiki := util.URLJoin(FullURL, "wiki", "WikiPage")
 	test("[WikiPage](WikiPage)",
 		`<p><a href="`+lnk+`" rel="nofollow">WikiPage</a></p>`,
 		`<p><a href="`+lnkWiki+`" rel="nofollow">WikiPage</a></p>`)
@@ -83,23 +85,22 @@ func TestRender_StandardLinks(t *testing.T) {
 
 func TestRender_Images(t *testing.T) {
 	setting.AppURL = AppURL
-	setting.AppSubURL = AppSubURL
 
 	test := func(input, expected string) {
 		buffer, err := markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: setting.AppSubURL,
+				Base: FullURL,
 			},
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 	}
 
 	url := "../../.images/src/02/train.jpg"
 	title := "Train"
 	href := "https://gitea.io"
-	result := util.URLJoin(AppSubURL, url)
+	result := util.URLJoin(FullURL, url)
 	// hint: With Markdown v2.5.2, there is a new syntax: [link](URL){:target="_blank"} , but we do not support it now
 
 	test(
@@ -132,11 +133,11 @@ func testAnswers(baseURLContent, baseURLImages string) []string {
 <li><a href="` + baseURLContent + `/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
 <li><a href="` + baseURLContent + `/Tips" rel="nofollow">Tips</a></li>
 </ul>
-<p>See commit <a href="http://localhost:3000/gogits/gogs/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p>
+<p>See commit <a href="/gogits/gogs/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p>
 <p>Ideas and codes</p>
 <ul>
-<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li>
-<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/gogits/gogs/issues/786" class="ref-issue" rel="nofollow">#786</a></li>
+<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li>
+<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/gogits/gogs/issues/786" class="ref-issue" rel="nofollow">#786</a></li>
 <li>Node graph editors <a href="https://github.com/ocornut/imgui/issues/306" rel="nofollow">https://github.com/ocornut/imgui/issues/306</a></li>
 <li><a href="` + baseURLContent + `/memory_editor_example" rel="nofollow">Memory Editor</a></li>
 <li><a href="` + baseURLContent + `/plot_var_example" rel="nofollow">Plot var helper</a></li>
@@ -289,33 +290,32 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno
 
 func TestTotal_RenderWiki(t *testing.T) {
 	setting.AppURL = AppURL
-	setting.AppSubURL = AppSubURL
 
-	answers := testAnswers(util.URLJoin(AppSubURL, "wiki"), util.URLJoin(AppSubURL, "wiki", "raw"))
+	answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw"))
 
 	for i := 0; i < len(sameCases); i++ {
 		line, err := markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: setting.AppSubURL,
+				Base: FullURL,
 			},
 			Metas:  localMetas,
 			IsWiki: true,
 		}, sameCases[i])
 		assert.NoError(t, err)
-		assert.Equal(t, answers[i], line)
+		assert.Equal(t, template.HTML(answers[i]), line)
 	}
 
 	testCases := []string{
 		// Guard wiki sidebar: special syntax
 		`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
 		// rendered
-		`<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
+		`<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
 `,
 		// special syntax
 		`[[Name|Link]]`,
 		// rendered
-		`<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p>
+		`<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p>
 `,
 	}
 
@@ -323,32 +323,31 @@ func TestTotal_RenderWiki(t *testing.T) {
 		line, err := markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: setting.AppSubURL,
+				Base: FullURL,
 			},
 			IsWiki: true,
 		}, testCases[i])
 		assert.NoError(t, err)
-		assert.Equal(t, testCases[i+1], line)
+		assert.Equal(t, template.HTML(testCases[i+1]), line)
 	}
 }
 
 func TestTotal_RenderString(t *testing.T) {
 	setting.AppURL = AppURL
-	setting.AppSubURL = AppSubURL
 
-	answers := testAnswers(util.URLJoin(AppSubURL, "src", "master"), util.URLJoin(AppSubURL, "media", "master"))
+	answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master"))
 
 	for i := 0; i < len(sameCases); i++ {
 		line, err := markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base:       AppSubURL,
+				Base:       FullURL,
 				BranchPath: "master",
 			},
 			Metas: localMetas,
 		}, sameCases[i])
 		assert.NoError(t, err)
-		assert.Equal(t, answers[i], line)
+		assert.Equal(t, template.HTML(answers[i]), line)
 	}
 
 	testCases := []string{}
@@ -357,11 +356,11 @@ func TestTotal_RenderString(t *testing.T) {
 		line, err := markdown.RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
 			Links: markup.Links{
-				Base: AppSubURL,
+				Base: FullURL,
 			},
 		}, testCases[i])
 		assert.NoError(t, err)
-		assert.Equal(t, testCases[i+1], line)
+		assert.Equal(t, template.HTML(testCases[i+1]), line)
 	}
 }
 
@@ -428,7 +427,7 @@ func TestRenderEmojiInLinks_Issue12331(t *testing.T) {
 `
 	res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
 	assert.NoError(t, err)
-	assert.Equal(t, expected, res)
+	assert.Equal(t, template.HTML(expected), res)
 }
 
 func TestColorPreview(t *testing.T) {
@@ -437,6 +436,10 @@ func TestColorPreview(t *testing.T) {
 		testcase string
 		expected string
 	}{
+		{ // do not render color names
+			"The CSS class `red` is there",
+			"<p>The CSS class <code>red</code> is there</p>\n",
+		},
 		{ // hex
 			"`#FF0000`",
 			`<p><code>#FF0000<span class="color-preview" style="background-color: #FF0000"></span></code></p>` + nl,
@@ -446,8 +449,8 @@ func TestColorPreview(t *testing.T) {
 			`<p><code>rgb(16, 32, 64)<span class="color-preview" style="background-color: rgb(16, 32, 64)"></span></code></p>` + nl,
 		},
 		{ // short hex
-			"This is the color white `#000`",
-			`<p>This is the color white <code>#000<span class="color-preview" style="background-color: #000"></span></code></p>` + nl,
+			"This is the color white `#0a0`",
+			`<p>This is the color white <code>#0a0<span class="color-preview" style="background-color: #0a0"></span></code></p>` + nl,
 		},
 		{ // hsl
 			"HSL stands for hue, saturation, and lightness. An example: `hsl(0, 100%, 50%)`.",
@@ -462,7 +465,7 @@ func TestColorPreview(t *testing.T) {
 	for _, test := range positiveTests {
 		res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
 		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
-		assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
+		assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
 
 	}
 
@@ -508,9 +511,17 @@ func TestMathBlock(t *testing.T) {
 			`\(a\) \(b\)`,
 			`<p><code class="language-math is-loading">a</code> <code class="language-math is-loading">b</code></p>` + nl,
 		},
+		{
+			`$a$.`,
+			`<p><code class="language-math is-loading">a</code>.</p>` + nl,
+		},
+		{
+			`.$a$`,
+			`<p>.$a$</p>` + nl,
+		},
 		{
 			`$a a$b b$`,
-			`<p><code class="language-math is-loading">a a$b b</code></p>` + nl,
+			`<p>$a a$b b$</p>` + nl,
 		},
 		{
 			`a a$b b`,
@@ -518,7 +529,15 @@ func TestMathBlock(t *testing.T) {
 		},
 		{
 			`a$b $a a$b b$`,
-			`<p>a$b <code class="language-math is-loading">a a$b b</code></p>` + nl,
+			`<p>a$b $a a$b b$</p>` + nl,
+		},
+		{
+			"a$x$",
+			`<p>a$x$</p>` + nl,
+		},
+		{
+			"$x$a",
+			`<p>$x$a</p>` + nl,
 		},
 		{
 			"$$a$$",
@@ -529,7 +548,7 @@ func TestMathBlock(t *testing.T) {
 	for _, test := range testcases {
 		res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
 		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
-		assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
+		assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
 
 	}
 }
@@ -567,12 +586,12 @@ foo: bar
 	for _, test := range testcases {
 		res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
 		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
-		assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
+		assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
 	}
 }
 
 func TestRenderLinks(t *testing.T) {
-	input := `  space @mention-user  
+	input := `  space @mention-user${SPACE}${SPACE}
 /just/a/path.bin
 https://example.com/file.bin
 [local link](file.bin)
@@ -593,8 +612,9 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 mail@domain.com
 @mention-user test
 #123
-  space  
+  space${SPACE}${SPACE}
 `
+	input = strings.ReplaceAll(input, "${SPACE}", " ") // replace ${SPACE} with " ", to avoid some editor's auto-trimming
 	cases := []struct {
 		Links    markup.Links
 		IsWiki   bool
@@ -633,9 +653,9 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -691,9 +711,9 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://gitea.io/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://gitea.io/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="https://gitea.io/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="https://gitea.io/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -749,9 +769,9 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -809,9 +829,9 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -869,9 +889,9 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -931,9 +951,9 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -957,6 +977,39 @@ space</p>
 	for i, c := range cases {
 		result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input)
 		assert.NoError(t, err, "Unexpected error in testcase: %v", i)
-		assert.Equal(t, c.Expected, result, "Unexpected result in testcase %v", i)
+		assert.Equal(t, template.HTML(c.Expected), result, "Unexpected result in testcase %v", i)
 	}
 }
+
+func TestAttention(t *testing.T) {
+	defer svg.MockIcon("octicon-info")()
+	defer svg.MockIcon("octicon-light-bulb")()
+	defer svg.MockIcon("octicon-report")()
+	defer svg.MockIcon("octicon-alert")()
+	defer svg.MockIcon("octicon-stop")()
+
+	renderAttention := func(attention, icon string) string {
+		tmpl := `<blockquote class="attention-header attention-{attention}"><p><svg class="attention-icon attention-{attention} svg {icon}" width="16" height="16"></svg><strong class="attention-{attention}">{Attention}</strong></p>`
+		tmpl = strings.ReplaceAll(tmpl, "{attention}", attention)
+		tmpl = strings.ReplaceAll(tmpl, "{icon}", icon)
+		tmpl = strings.ReplaceAll(tmpl, "{Attention}", cases.Title(language.English).String(attention))
+		return tmpl
+	}
+
+	test := func(input, expected string) {
+		result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background()}, input)
+		assert.NoError(t, err)
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result)))
+	}
+
+	test(`
+> [!NOTE]
+> text
+`, renderAttention("note", "octicon-info")+"\n<p>text</p>\n</blockquote>")
+
+	test(`> [!note]`, renderAttention("note", "octicon-info")+"\n</blockquote>")
+	test(`> [!tip]`, renderAttention("tip", "octicon-light-bulb")+"\n</blockquote>")
+	test(`> [!important]`, renderAttention("important", "octicon-report")+"\n</blockquote>")
+	test(`> [!warning]`, renderAttention("warning", "octicon-alert")+"\n</blockquote>")
+	test(`> [!caution]`, renderAttention("caution", "octicon-stop")+"\n</blockquote>")
+}
diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go
index 0ac25c2b2a..862234e69b 100644
--- a/modules/markup/markdown/math/inline_parser.go
+++ b/modules/markup/markdown/math/inline_parser.go
@@ -41,9 +41,12 @@ func (parser *inlineParser) Trigger() []byte {
 	return parser.start[0:1]
 }
 
+func isPunctuation(b byte) bool {
+	return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
+}
+
 func isAlphanumeric(b byte) bool {
-	// Github only cares about 0-9A-Za-z
-	return (b >= '0' && b <= '9') || (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
+	return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
 }
 
 // Parse parses the current line and returns a result of parsing.
@@ -56,7 +59,7 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
 	}
 
 	precedingCharacter := block.PrecendingCharacter()
-	if precedingCharacter < 256 && isAlphanumeric(byte(precedingCharacter)) {
+	if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
 		// need to exclude things like `a$` from being considered a start
 		return nil
 	}
@@ -75,14 +78,19 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
 		ender += pos
 
 		// Now we want to check the character at the end of our parser section
-		// that is ender + len(parser.end)
+		// that is ender + len(parser.end) and check if char before ender is '\'
 		pos = ender + len(parser.end)
 		if len(line) <= pos {
 			break
 		}
-		if !isAlphanumeric(line[pos]) {
+		suceedingCharacter := line[pos]
+		if !isPunctuation(suceedingCharacter) && !(suceedingCharacter == ' ') {
+			return nil
+		}
+		if line[ender-1] != '\\' {
 			break
 		}
+
 		// move the pointer onwards
 		ender += len(parser.end)
 	}
diff --git a/modules/markup/markdown/prefixed_id.go b/modules/markup/markdown/prefixed_id.go
new file mode 100644
index 0000000000..9c60949202
--- /dev/null
+++ b/modules/markup/markdown/prefixed_id.go
@@ -0,0 +1,59 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"bytes"
+	"fmt"
+
+	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/markup/common"
+
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/util"
+)
+
+type prefixedIDs struct {
+	values container.Set[string]
+}
+
+// Generate generates a new element id.
+func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
+	dft := []byte("id")
+	if kind == ast.KindHeading {
+		dft = []byte("heading")
+	}
+	return p.GenerateWithDefault(value, dft)
+}
+
+// GenerateWithDefault generates a new element id.
+func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
+	result := common.CleanValue(value)
+	if len(result) == 0 {
+		result = dft
+	}
+	if !bytes.HasPrefix(result, []byte("user-content-")) {
+		result = append([]byte("user-content-"), result...)
+	}
+	if p.values.Add(util.BytesToReadOnlyString(result)) {
+		return result
+	}
+	for i := 1; ; i++ {
+		newResult := fmt.Sprintf("%s-%d", result, i)
+		if p.values.Add(newResult) {
+			return []byte(newResult)
+		}
+	}
+}
+
+// Put puts a given element id to the used ids table.
+func (p *prefixedIDs) Put(value []byte) {
+	p.values.Add(util.BytesToReadOnlyString(value))
+}
+
+func newPrefixedIDs() *prefixedIDs {
+	return &prefixedIDs{
+		values: make(container.Set[string]),
+	}
+}
diff --git a/modules/markup/markdown/toc.go b/modules/markup/markdown/toc.go
index 9602040931..38f744a25f 100644
--- a/modules/markup/markdown/toc.go
+++ b/modules/markup/markdown/toc.go
@@ -21,7 +21,7 @@ func createTOCNode(toc []markup.Header, lang string, detailsAttrs map[string]str
 		details.SetAttributeString(k, []byte(v))
 	}
 
-	summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).Tr("toc"))))
+	summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).TrString("toc"))))
 	details.AppendChild(details, summary)
 	ul := ast.NewList('-')
 	details.AppendChild(details, ul)
diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go
new file mode 100644
index 0000000000..933f0e5c59
--- /dev/null
+++ b/modules/markup/markdown/transform_blockquote.go
@@ -0,0 +1,98 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"strings"
+
+	"code.gitea.io/gitea/modules/svg"
+
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/text"
+	"github.com/yuin/goldmark/util"
+	"golang.org/x/text/cases"
+	"golang.org/x/text/language"
+)
+
+// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
+func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	if entering {
+		n := node.(*Attention)
+		var octiconName string
+		switch n.AttentionType {
+		case "tip":
+			octiconName = "light-bulb"
+		case "important":
+			octiconName = "report"
+		case "warning":
+			octiconName = "alert"
+		case "caution":
+			octiconName = "stop"
+		default: // including "note"
+			octiconName = "info"
+		}
+		_, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
+	}
+	return ast.WalkContinue, nil
+}
+
+func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) {
+	// We only want attention blockquotes when the AST looks like:
+	// > Text("[") Text("!TYPE") Text("]")
+
+	// grab these nodes and make sure we adhere to the attention blockquote structure
+	firstParagraph := v.FirstChild()
+	g.applyElementDir(firstParagraph)
+	if firstParagraph.ChildCount() < 3 {
+		return ast.WalkContinue, nil
+	}
+	node1, ok := firstParagraph.FirstChild().(*ast.Text)
+	if !ok {
+		return ast.WalkContinue, nil
+	}
+	node2, ok := node1.NextSibling().(*ast.Text)
+	if !ok {
+		return ast.WalkContinue, nil
+	}
+	node3, ok := node2.NextSibling().(*ast.Text)
+	if !ok {
+		return ast.WalkContinue, nil
+	}
+	val1 := string(node1.Segment.Value(reader.Source()))
+	val2 := string(node2.Segment.Value(reader.Source()))
+	val3 := string(node3.Segment.Value(reader.Source()))
+	if val1 != "[" || val3 != "]" || !strings.HasPrefix(val2, "!") {
+		return ast.WalkContinue, nil
+	}
+
+	// grab attention type from markdown source
+	attentionType := strings.ToLower(val2[1:])
+	if !g.attentionTypes.Contains(attentionType) {
+		return ast.WalkContinue, nil
+	}
+
+	// color the blockquote
+	v.SetAttributeString("class", []byte("attention-header attention-"+attentionType))
+
+	// create an emphasis to make it bold
+	attentionParagraph := ast.NewParagraph()
+	g.applyElementDir(attentionParagraph)
+	emphasis := ast.NewEmphasis(2)
+	emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
+
+	attentionAstString := ast.NewString([]byte(cases.Title(language.English).String(attentionType)))
+
+	// replace the ![TYPE] with a dedicated paragraph of icon+Type
+	emphasis.AppendChild(emphasis, attentionAstString)
+	attentionParagraph.AppendChild(attentionParagraph, NewAttention(attentionType))
+	attentionParagraph.AppendChild(attentionParagraph, emphasis)
+	firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph)
+	firstParagraph.RemoveChild(firstParagraph, node1)
+	firstParagraph.RemoveChild(firstParagraph, node2)
+	firstParagraph.RemoveChild(firstParagraph, node3)
+	if firstParagraph.ChildCount() == 0 {
+		firstParagraph.Parent().RemoveChild(firstParagraph.Parent(), firstParagraph)
+	}
+	return ast.WalkContinue, nil
+}
diff --git a/modules/markup/markdown/transform_codespan.go b/modules/markup/markdown/transform_codespan.go
new file mode 100644
index 0000000000..5b07d72999
--- /dev/null
+++ b/modules/markup/markdown/transform_codespan.go
@@ -0,0 +1,76 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+
+	"code.gitea.io/gitea/modules/markup"
+
+	"github.com/microcosm-cc/bluemonday/css"
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/renderer/html"
+	"github.com/yuin/goldmark/text"
+	"github.com/yuin/goldmark/util"
+)
+
+// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
+// See #21474 for reference
+func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+	if entering {
+		if n.Attributes() != nil {
+			_, _ = w.WriteString("<code")
+			html.RenderAttributes(w, n, html.CodeAttributeFilter)
+			_ = w.WriteByte('>')
+		} else {
+			_, _ = w.WriteString("<code>")
+		}
+		for c := n.FirstChild(); c != nil; c = c.NextSibling() {
+			switch v := c.(type) {
+			case *ast.Text:
+				segment := v.Segment
+				value := segment.Value(source)
+				if bytes.HasSuffix(value, []byte("\n")) {
+					r.Writer.RawWrite(w, value[:len(value)-1])
+					r.Writer.RawWrite(w, []byte(" "))
+				} else {
+					r.Writer.RawWrite(w, value)
+				}
+			case *ColorPreview:
+				_, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
+			}
+		}
+		return ast.WalkSkipChildren, nil
+	}
+	_, _ = w.WriteString("</code>")
+	return ast.WalkContinue, nil
+}
+
+// cssColorHandler checks if a string is a render-able CSS color value.
+// The code is from "github.com/microcosm-cc/bluemonday/css.ColorHandler", except that it doesn't handle color words like "red".
+func cssColorHandler(value string) bool {
+	value = strings.ToLower(value)
+	if css.HexRGB.MatchString(value) {
+		return true
+	}
+	if css.RGB.MatchString(value) {
+		return true
+	}
+	if css.RGBA.MatchString(value) {
+		return true
+	}
+	if css.HSL.MatchString(value) {
+		return true
+	}
+	return css.HSLA.MatchString(value)
+}
+
+func (g *ASTTransformer) transformCodeSpan(ctx *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) {
+	colorContent := v.Text(reader.Source())
+	if cssColorHandler(string(colorContent)) {
+		v.AppendChild(v, NewColorPreview(colorContent))
+	}
+}
diff --git a/modules/markup/markdown/transform_heading.go b/modules/markup/markdown/transform_heading.go
new file mode 100644
index 0000000000..ce585a37de
--- /dev/null
+++ b/modules/markup/markdown/transform_heading.go
@@ -0,0 +1,32 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"fmt"
+
+	"code.gitea.io/gitea/modules/markup"
+
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/text"
+	"github.com/yuin/goldmark/util"
+)
+
+func (g *ASTTransformer) transformHeading(ctx *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) {
+	for _, attr := range v.Attributes() {
+		if _, ok := attr.Value.([]byte); !ok {
+			v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
+		}
+	}
+	txt := v.Text(reader.Source())
+	header := markup.Header{
+		Text:  util.BytesToReadOnlyString(txt),
+		Level: v.Level,
+	}
+	if id, found := v.AttributeString("id"); found {
+		header.ID = util.BytesToReadOnlyString(id.([]byte))
+	}
+	*tocList = append(*tocList, header)
+	g.applyElementDir(v)
+}
diff --git a/modules/markup/markdown/transform_image.go b/modules/markup/markdown/transform_image.go
new file mode 100644
index 0000000000..f290dc3721
--- /dev/null
+++ b/modules/markup/markdown/transform_image.go
@@ -0,0 +1,66 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"strings"
+
+	"code.gitea.io/gitea/modules/markup"
+	giteautil "code.gitea.io/gitea/modules/util"
+
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/text"
+)
+
+func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image, reader text.Reader) {
+	// Images need two things:
+	//
+	// 1. Their src needs to munged to be a real value
+	// 2. If they're not wrapped with a link they need a link wrapper
+
+	// Check if the destination is a real link
+	if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
+		v.Destination = []byte(giteautil.URLJoin(
+			ctx.Links.ResolveMediaLink(ctx.IsWiki),
+			strings.TrimLeft(string(v.Destination), "/"),
+		))
+	}
+
+	parent := v.Parent()
+	// Create a link around image only if parent is not already a link
+	if _, ok := parent.(*ast.Link); !ok && parent != nil {
+		next := v.NextSibling()
+
+		// Create a link wrapper
+		wrap := ast.NewLink()
+		wrap.Destination = v.Destination
+		wrap.Title = v.Title
+		wrap.SetAttributeString("target", []byte("_blank"))
+
+		// Duplicate the current image node
+		image := ast.NewImage(ast.NewLink())
+		image.Destination = v.Destination
+		image.Title = v.Title
+		for _, attr := range v.Attributes() {
+			image.SetAttribute(attr.Name, attr.Value)
+		}
+		for child := v.FirstChild(); child != nil; {
+			next := child.NextSibling()
+			image.AppendChild(image, child)
+			child = next
+		}
+
+		// Append our duplicate image to the wrapper link
+		wrap.AppendChild(wrap, image)
+
+		// Wire in the next sibling
+		wrap.SetNextSibling(next)
+
+		// Replace the current node with the wrapper link
+		parent.ReplaceChild(parent, v, wrap)
+
+		// But most importantly ensure the next sibling is still on the old image too
+		v.SetNextSibling(next)
+	}
+}
diff --git a/modules/markup/markdown/transform_link.go b/modules/markup/markdown/transform_link.go
new file mode 100644
index 0000000000..7e305b74bc
--- /dev/null
+++ b/modules/markup/markdown/transform_link.go
@@ -0,0 +1,42 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"path/filepath"
+
+	"code.gitea.io/gitea/modules/markup"
+	giteautil "code.gitea.io/gitea/modules/util"
+
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/text"
+)
+
+func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link, reader text.Reader) {
+	// Links need their href to munged to be a real value
+	link := v.Destination
+	isAnchorFragment := len(link) > 0 && link[0] == '#'
+	if !isAnchorFragment && !markup.IsFullURLBytes(link) {
+		base := ctx.Links.Base
+		if ctx.IsWiki {
+			if filepath.Ext(string(link)) == "" {
+				// This link doesn't have a file extension - assume a regular wiki link
+				base = ctx.Links.WikiLink()
+			} else if markup.Type(string(link)) != "" {
+				// If it's a file type we can render, use a regular wiki link
+				base = ctx.Links.WikiLink()
+			} else {
+				// Otherwise, use a raw link instead
+				base = ctx.Links.WikiRawLink()
+			}
+		} else if ctx.Links.HasBranchInfo() {
+			base = ctx.Links.SrcLink()
+		}
+		link = []byte(giteautil.URLJoin(base, string(link)))
+	}
+	if isAnchorFragment {
+		link = []byte("#user-content-" + string(link)[1:])
+	}
+	v.Destination = link
+}
diff --git a/modules/markup/markdown/transform_list.go b/modules/markup/markdown/transform_list.go
new file mode 100644
index 0000000000..6563e2dd64
--- /dev/null
+++ b/modules/markup/markdown/transform_list.go
@@ -0,0 +1,86 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"fmt"
+
+	"code.gitea.io/gitea/modules/markup"
+
+	"github.com/yuin/goldmark/ast"
+	east "github.com/yuin/goldmark/extension/ast"
+	"github.com/yuin/goldmark/renderer/html"
+	"github.com/yuin/goldmark/text"
+	"github.com/yuin/goldmark/util"
+)
+
+func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*TaskCheckBoxListItem)
+	if entering {
+		if n.Attributes() != nil {
+			_, _ = w.WriteString("<li")
+			html.RenderAttributes(w, n, html.ListItemAttributeFilter)
+			_ = w.WriteByte('>')
+		} else {
+			_, _ = w.WriteString("<li>")
+		}
+		fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition)
+		if n.IsChecked {
+			_, _ = w.WriteString(` checked=""`)
+		}
+		if r.XHTML {
+			_, _ = w.WriteString(` />`)
+		} else {
+			_ = w.WriteByte('>')
+		}
+		fc := n.FirstChild()
+		if fc != nil {
+			if _, ok := fc.(*ast.TextBlock); !ok {
+				_ = w.WriteByte('\n')
+			}
+		}
+	} else {
+		_, _ = w.WriteString("</li>\n")
+	}
+	return ast.WalkContinue, nil
+}
+
+func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	return ast.WalkContinue, nil
+}
+
+func (g *ASTTransformer) transformList(ctx *markup.RenderContext, v *ast.List, reader text.Reader, rc *RenderConfig) {
+	if v.HasChildren() {
+		children := make([]ast.Node, 0, v.ChildCount())
+		child := v.FirstChild()
+		for child != nil {
+			children = append(children, child)
+			child = child.NextSibling()
+		}
+		v.RemoveChildren(v)
+
+		for _, child := range children {
+			listItem := child.(*ast.ListItem)
+			if !child.HasChildren() || !child.FirstChild().HasChildren() {
+				v.AppendChild(v, child)
+				continue
+			}
+			taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
+			if !ok {
+				v.AppendChild(v, child)
+				continue
+			}
+			newChild := NewTaskCheckBoxListItem(listItem)
+			newChild.IsChecked = taskCheckBox.IsChecked
+			newChild.SetAttributeString("class", []byte("task-list-item"))
+			segments := newChild.FirstChild().Lines()
+			if segments.Len() > 0 {
+				segment := segments.At(0)
+				newChild.SourcePosition = rc.metaLength + segment.Start
+			}
+			v.AppendChild(v, newChild)
+		}
+	}
+	g.applyElementDir(v)
+}
diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go
index ac1cedff6d..25f8d15ef4 100644
--- a/modules/markup/orgmode/orgmode.go
+++ b/modules/markup/orgmode/orgmode.go
@@ -136,17 +136,24 @@ type Writer struct {
 func (r *Writer) resolveLink(kind, link string) string {
 	link = strings.TrimPrefix(link, "file:")
 	if !strings.HasPrefix(link, "#") && // not a URL fragment
-		!markup.IsLinkStr(link) && // not an absolute URL
-		!strings.HasPrefix(link, "mailto:") {
+		!markup.IsFullURLString(link) {
 		if kind == "regular" {
 			// orgmode reports the link kind as "regular" for "[[ImageLink.svg][The Image Desc]]"
 			// so we need to try to guess the link kind again here
 			kind = org.RegularLink{URL: link}.Kind()
 		}
+
 		base := r.Ctx.Links.Base
+		if r.Ctx.IsWiki {
+			base = r.Ctx.Links.WikiLink()
+		} else if r.Ctx.Links.HasBranchInfo() {
+			base = r.Ctx.Links.SrcLink()
+		}
+
 		if kind == "image" || kind == "video" {
 			base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki)
 		}
+
 		link = util.URLJoin(base, link)
 	}
 	return link
diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go
index 95f53c9cc9..75b60ed81f 100644
--- a/modules/markup/orgmode/orgmode_test.go
+++ b/modules/markup/orgmode/orgmode_test.go
@@ -19,6 +19,30 @@ const AppURL = "http://localhost:3000/"
 func TestRender_StandardLinks(t *testing.T) {
 	setting.AppURL = AppURL
 
+	test := func(input, expected string, isWiki bool) {
+		buffer, err := RenderString(&markup.RenderContext{
+			Ctx: git.DefaultContext,
+			Links: markup.Links{
+				Base:       "/relative-path",
+				BranchPath: "branch/main",
+			},
+			IsWiki: isWiki,
+		}, input)
+		assert.NoError(t, err)
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+	}
+
+	test("[[https://google.com/]]",
+		`<p><a href="https://google.com/">https://google.com/</a></p>`, false)
+	test("[[WikiPage][The WikiPage Desc]]",
+		`<p><a href="/relative-path/wiki/WikiPage">The WikiPage Desc</a></p>`, true)
+	test("[[ImageLink.svg][The Image Desc]]",
+		`<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`, false)
+}
+
+func TestRender_InternalLinks(t *testing.T) {
+	setting.AppURL = AppURL
+
 	test := func(input, expected string) {
 		buffer, err := RenderString(&markup.RenderContext{
 			Ctx: git.DefaultContext,
@@ -31,12 +55,14 @@ func TestRender_StandardLinks(t *testing.T) {
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 	}
 
-	test("[[https://google.com/]]",
-		`<p><a href="https://google.com/">https://google.com/</a></p>`)
-	test("[[WikiPage][The WikiPage Desc]]",
-		`<p><a href="/relative-path/WikiPage">The WikiPage Desc</a></p>`)
-	test("[[ImageLink.svg][The Image Desc]]",
-		`<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`)
+	test("[[file:test.org][Test]]",
+		`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`)
+	test("[[./test.org][Test]]",
+		`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`)
+	test("[[test.org][Test]]",
+		`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`)
+	test("[[path/to/test.org][Test]]",
+		`<p><a href="/relative-path/src/branch/main/path/to/test.org">Test</a></p>`)
 }
 
 func TestRender_Media(t *testing.T) {
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 5a7adcc553..005fcc278b 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -8,6 +8,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"html/template"
 	"io"
 	"net/url"
 	"path/filepath"
@@ -33,6 +34,8 @@ type ProcessorHelper struct {
 	IsUsernameMentionable func(ctx context.Context, username string) bool
 
 	ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
+
+	RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
 }
 
 var DefaultProcessorHelper ProcessorHelper
@@ -82,9 +85,17 @@ type RenderContext struct {
 }
 
 type Links struct {
-	Base       string
-	BranchPath string
-	TreePath   string
+	AbsolutePrefix bool
+	Base           string
+	BranchPath     string
+	TreePath       string
+}
+
+func (l *Links) Prefix() string {
+	if l.AbsolutePrefix {
+		return setting.AppURL
+	}
+	return setting.AppSubURL
 }
 
 func (l *Links) HasBranchInfo() bool {
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index 992e85b989..570a1da248 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -60,13 +60,28 @@ func createDefaultPolicy() *bluemonday.Policy {
 	// For JS code copy and Mermaid loading state
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
 
+	// For code preview
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
+	policy.AllowAttrs("data-line-number").OnElements("span")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
+
+	// For code preview (unicode escape)
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
+	policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
+
 	// For color preview
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
 
 	// For attention
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+$`)).OnElements("span", "strong")
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-\w+$`)).OnElements("svg")
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
 	policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
 	policy.AllowAttrs("fill-rule", "d").OnElements("path")
 
@@ -104,18 +119,12 @@ func createDefaultPolicy() *bluemonday.Policy {
 	// Allow icons
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
 
-	// Allow unlabelled labels
-	policy.AllowNoAttrs().OnElements("label")
-
 	// Allow classes for emojis
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
 
 	// Allow icons, emojis, chroma syntax and keyword markup on span
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
 
-	// Allow 'style' attribute on text elements.
-	policy.AllowAttrs("style").OnElements("span", "p")
-
 	// Allow 'color' and 'background-color' properties for the style attribute on text elements.
 	policy.AllowStyles("color", "background-color").OnElements("span", "p")
 
@@ -143,7 +152,7 @@ func createDefaultPolicy() *bluemonday.Policy {
 
 	generalSafeElements := []string{
 		"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
-		"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote",
+		"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
 		"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
 		"details", "caption", "figure", "figcaption",
 		"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
diff --git a/modules/migration/messenger.go b/modules/migration/messenger.go
index 924aac9769..6f9cad3f10 100644
--- a/modules/migration/messenger.go
+++ b/modules/migration/messenger.go
@@ -3,7 +3,7 @@
 
 package migration
 
-// Messenger is a formatting function similar to i18n.Tr
+// Messenger is a formatting function similar to i18n.TrString
 type Messenger func(key string, args ...any)
 
 // NilMessenger represents an empty formatting function
diff --git a/modules/optional/option_test.go b/modules/optional/option_test.go
index 7ec345b6ba..4f55608004 100644
--- a/modules/optional/option_test.go
+++ b/modules/optional/option_test.go
@@ -1,48 +1,59 @@
 // Copyright 2024 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
-package optional
+package optional_test
 
 import (
 	"testing"
 
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 
 	"github.com/stretchr/testify/assert"
 )
 
 func TestOption(t *testing.T) {
-	var uninitialized Option[int]
+	var uninitialized optional.Option[int]
 	assert.False(t, uninitialized.Has())
 	assert.Equal(t, int(0), uninitialized.Value())
 	assert.Equal(t, int(1), uninitialized.ValueOrDefault(1))
 
-	none := None[int]()
+	none := optional.None[int]()
 	assert.False(t, none.Has())
 	assert.Equal(t, int(0), none.Value())
 	assert.Equal(t, int(1), none.ValueOrDefault(1))
 
-	some := Some[int](1)
+	some := optional.Some[int](1)
 	assert.True(t, some.Has())
 	assert.Equal(t, int(1), some.Value())
 	assert.Equal(t, int(1), some.ValueOrDefault(2))
 
-	var ptr *int
-	assert.False(t, FromPtr(ptr).Has())
+	noneBool := optional.None[bool]()
+	assert.False(t, noneBool.Has())
+	assert.False(t, noneBool.Value())
+	assert.True(t, noneBool.ValueOrDefault(true))
 
-	opt1 := FromPtr(util.ToPointer(1))
+	someBool := optional.Some(true)
+	assert.True(t, someBool.Has())
+	assert.True(t, someBool.Value())
+	assert.True(t, someBool.ValueOrDefault(false))
+
+	var ptr *int
+	assert.False(t, optional.FromPtr(ptr).Has())
+
+	int1 := 1
+	opt1 := optional.FromPtr(&int1)
 	assert.True(t, opt1.Has())
 	assert.Equal(t, int(1), opt1.Value())
 
-	assert.False(t, FromNonDefault("").Has())
+	assert.False(t, optional.FromNonDefault("").Has())
 
-	opt2 := FromNonDefault("test")
+	opt2 := optional.FromNonDefault("test")
 	assert.True(t, opt2.Has())
 	assert.Equal(t, "test", opt2.Value())
 
-	assert.False(t, FromNonDefault(0).Has())
+	assert.False(t, optional.FromNonDefault(0).Has())
 
-	opt3 := FromNonDefault(1)
+	opt3 := optional.FromNonDefault(1)
 	assert.True(t, opt3.Has())
 	assert.Equal(t, int(1), opt3.Value())
 }
diff --git a/modules/optional/serialization.go b/modules/optional/serialization.go
new file mode 100644
index 0000000000..6688e78cd1
--- /dev/null
+++ b/modules/optional/serialization.go
@@ -0,0 +1,46 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package optional
+
+import (
+	"code.gitea.io/gitea/modules/json"
+
+	"gopkg.in/yaml.v3"
+)
+
+func (o *Option[T]) UnmarshalJSON(data []byte) error {
+	var v *T
+	if err := json.Unmarshal(data, &v); err != nil {
+		return err
+	}
+	*o = FromPtr(v)
+	return nil
+}
+
+func (o Option[T]) MarshalJSON() ([]byte, error) {
+	if !o.Has() {
+		return []byte("null"), nil
+	}
+
+	return json.Marshal(o.Value())
+}
+
+func (o *Option[T]) UnmarshalYAML(value *yaml.Node) error {
+	var v *T
+	if err := value.Decode(&v); err != nil {
+		return err
+	}
+	*o = FromPtr(v)
+	return nil
+}
+
+func (o Option[T]) MarshalYAML() (interface{}, error) {
+	if !o.Has() {
+		return nil, nil
+	}
+
+	value := new(yaml.Node)
+	err := value.Encode(o.Value())
+	return value, err
+}
diff --git a/modules/optional/serialization_test.go b/modules/optional/serialization_test.go
new file mode 100644
index 0000000000..09a4bddea0
--- /dev/null
+++ b/modules/optional/serialization_test.go
@@ -0,0 +1,190 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package optional_test
+
+import (
+	std_json "encoding/json" //nolint:depguard
+	"testing"
+
+	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/optional"
+
+	"github.com/stretchr/testify/assert"
+	"gopkg.in/yaml.v3"
+)
+
+type testSerializationStruct struct {
+	NormalString string                  `json:"normal_string" yaml:"normal_string"`
+	NormalBool   bool                    `json:"normal_bool" yaml:"normal_bool"`
+	OptBool      optional.Option[bool]   `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"`
+	OptString    optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"`
+	OptTwoBool   optional.Option[bool]   `json:"optional_two_bool" yaml:"optional_two_bool"`
+	OptTwoString optional.Option[string] `json:"optional_twostring" yaml:"optional_two_string"`
+}
+
+func TestOptionalToJson(t *testing.T) {
+	tests := []struct {
+		name string
+		obj  *testSerializationStruct
+		want string
+	}{
+		{
+			name: "empty",
+			obj:  new(testSerializationStruct),
+			want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_twostring":null}`,
+		},
+		{
+			name: "some",
+			obj: &testSerializationStruct{
+				NormalString: "a string",
+				NormalBool:   true,
+				OptBool:      optional.Some(false),
+				OptString:    optional.Some(""),
+				OptTwoBool:   optional.None[bool](),
+				OptTwoString: optional.None[string](),
+			},
+			want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			b, err := json.Marshal(tc.obj)
+			assert.NoError(t, err)
+			assert.EqualValues(t, tc.want, string(b), "gitea json module returned unexpected")
+
+			b, err = std_json.Marshal(tc.obj)
+			assert.NoError(t, err)
+			assert.EqualValues(t, tc.want, string(b), "std json module returned unexpected")
+		})
+	}
+}
+
+func TestOptionalFromJson(t *testing.T) {
+	tests := []struct {
+		name string
+		data string
+		want testSerializationStruct
+	}{
+		{
+			name: "empty",
+			data: `{}`,
+			want: testSerializationStruct{
+				NormalString: "",
+			},
+		},
+		{
+			name: "some",
+			data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
+			want: testSerializationStruct{
+				NormalString: "a string",
+				NormalBool:   true,
+				OptBool:      optional.Some(false),
+				OptString:    optional.Some(""),
+			},
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			var obj1 testSerializationStruct
+			err := json.Unmarshal([]byte(tc.data), &obj1)
+			assert.NoError(t, err)
+			assert.EqualValues(t, tc.want, obj1, "gitea json module returned unexpected")
+
+			var obj2 testSerializationStruct
+			err = std_json.Unmarshal([]byte(tc.data), &obj2)
+			assert.NoError(t, err)
+			assert.EqualValues(t, tc.want, obj2, "std json module returned unexpected")
+		})
+	}
+}
+
+func TestOptionalToYaml(t *testing.T) {
+	tests := []struct {
+		name string
+		obj  *testSerializationStruct
+		want string
+	}{
+		{
+			name: "empty",
+			obj:  new(testSerializationStruct),
+			want: `normal_string: ""
+normal_bool: false
+optional_two_bool: null
+optional_two_string: null
+`,
+		},
+		{
+			name: "some",
+			obj: &testSerializationStruct{
+				NormalString: "a string",
+				NormalBool:   true,
+				OptBool:      optional.Some(false),
+				OptString:    optional.Some(""),
+			},
+			want: `normal_string: a string
+normal_bool: true
+optional_bool: false
+optional_string: ""
+optional_two_bool: null
+optional_two_string: null
+`,
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			b, err := yaml.Marshal(tc.obj)
+			assert.NoError(t, err)
+			assert.EqualValues(t, tc.want, string(b), "yaml module returned unexpected")
+		})
+	}
+}
+
+func TestOptionalFromYaml(t *testing.T) {
+	tests := []struct {
+		name string
+		data string
+		want testSerializationStruct
+	}{
+		{
+			name: "empty",
+			data: ``,
+			want: testSerializationStruct{},
+		},
+		{
+			name: "empty but init",
+			data: `normal_string: ""
+normal_bool: false
+optional_bool:
+optional_two_bool:
+optional_two_string:
+`,
+			want: testSerializationStruct{},
+		},
+		{
+			name: "some",
+			data: `
+normal_string: a string
+normal_bool: true
+optional_bool: false
+optional_string: ""
+optional_two_bool: null
+optional_twostring: null
+`,
+			want: testSerializationStruct{
+				NormalString: "a string",
+				NormalBool:   true,
+				OptBool:      optional.Some(false),
+				OptString:    optional.Some(""),
+			},
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			var obj testSerializationStruct
+			err := yaml.Unmarshal([]byte(tc.data), &obj)
+			assert.NoError(t, err)
+			assert.EqualValues(t, tc.want, obj, "yaml module returned unexpected")
+		})
+	}
+}
diff --git a/modules/packages/alpine/metadata.go b/modules/packages/alpine/metadata.go
index 582c42610d..c492811744 100644
--- a/modules/packages/alpine/metadata.go
+++ b/modules/packages/alpine/metadata.go
@@ -34,6 +34,8 @@ const (
 
 	RepositoryPackage = "_alpine"
 	RepositoryVersion = "_repository"
+
+	NoArch = "noarch"
 )
 
 // https://wiki.alpinelinux.org/wiki/Apk_spec
diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go
index 3c478b1c02..6769c514cc 100644
--- a/modules/packages/nuget/metadata.go
+++ b/modules/packages/nuget/metadata.go
@@ -58,6 +58,7 @@ type Package struct {
 type Metadata struct {
 	Description              string                  `json:"description,omitempty"`
 	ReleaseNotes             string                  `json:"release_notes,omitempty"`
+	Readme                   string                  `json:"readme,omitempty"`
 	Authors                  string                  `json:"authors,omitempty"`
 	ProjectURL               string                  `json:"project_url,omitempty"`
 	RepositoryURL            string                  `json:"repository_url,omitempty"`
@@ -71,6 +72,7 @@ type Dependency struct {
 	Version string `json:"version"`
 }
 
+// https://learn.microsoft.com/en-us/nuget/reference/nuspec
 type nuspecPackage struct {
 	Metadata struct {
 		ID                       string `xml:"id"`
@@ -80,6 +82,7 @@ type nuspecPackage struct {
 		ProjectURL               string `xml:"projectUrl"`
 		Description              string `xml:"description"`
 		ReleaseNotes             string `xml:"releaseNotes"`
+		Readme                   string `xml:"readme"`
 		PackageTypes             struct {
 			PackageType []struct {
 				Name string `xml:"name,attr"`
@@ -89,6 +92,11 @@ type nuspecPackage struct {
 			URL string `xml:"url,attr"`
 		} `xml:"repository"`
 		Dependencies struct {
+			Dependency []struct {
+				ID      string `xml:"id,attr"`
+				Version string `xml:"version,attr"`
+				Exclude string `xml:"exclude,attr"`
+			} `xml:"dependency"`
 			Group []struct {
 				TargetFramework string `xml:"targetFramework,attr"`
 				Dependency      []struct {
@@ -122,14 +130,14 @@ func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
 			}
 			defer f.Close()
 
-			return ParseNuspecMetaData(f)
+			return ParseNuspecMetaData(archive, f)
 		}
 	}
 	return nil, ErrMissingNuspecFile
 }
 
 // ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
-func ParseNuspecMetaData(r io.Reader) (*Package, error) {
+func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
 	var p nuspecPackage
 	if err := xml.NewDecoder(r).Decode(&p); err != nil {
 		return nil, err
@@ -166,6 +174,28 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) {
 		Dependencies:             make(map[string][]Dependency),
 	}
 
+	if p.Metadata.Readme != "" {
+		f, err := archive.Open(p.Metadata.Readme)
+		if err == nil {
+			buf, _ := io.ReadAll(f)
+			m.Readme = string(buf)
+			_ = f.Close()
+		}
+	}
+
+	if len(p.Metadata.Dependencies.Dependency) > 0 {
+		deps := make([]Dependency, 0, len(p.Metadata.Dependencies.Dependency))
+		for _, dep := range p.Metadata.Dependencies.Dependency {
+			if dep.ID == "" || dep.Version == "" {
+				continue
+			}
+			deps = append(deps, Dependency{
+				ID:      dep.ID,
+				Version: dep.Version,
+			})
+		}
+		m.Dependencies[""] = deps
+	}
 	for _, group := range p.Metadata.Dependencies.Group {
 		deps := make([]Dependency, 0, len(group.Dependency))
 		for _, dep := range group.Dependency {
diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go
index bba2bff4a5..f466492f8a 100644
--- a/modules/packages/nuget/metadata_test.go
+++ b/modules/packages/nuget/metadata_test.go
@@ -6,7 +6,6 @@ package nuget
 import (
 	"archive/zip"
 	"bytes"
-	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -19,6 +18,7 @@ const (
 	projectURL        = "https://gitea.io"
 	description       = "Package Description"
 	releaseNotes      = "Package Release Notes"
+	readme            = "Readme"
 	repositoryURL     = "https://gitea.io/gitea/gitea"
 	targetFramework   = ".NETStandard2.1"
 	dependencyID      = "System.Text.Json"
@@ -36,6 +36,7 @@ const nuspecContent = `<?xml version="1.0" encoding="utf-8"?>
     <description>` + description + `</description>
     <releaseNotes>` + releaseNotes + `</releaseNotes>
     <repository url="` + repositoryURL + `" />
+    <readme>README.md</readme>
     <dependencies>
       <group targetFramework="` + targetFramework + `">
         <dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" />
@@ -60,17 +61,19 @@ const symbolsNuspecContent = `<?xml version="1.0" encoding="utf-8"?>
 </package>`
 
 func TestParsePackageMetaData(t *testing.T) {
-	createArchive := func(name, content string) []byte {
+	createArchive := func(files map[string]string) []byte {
 		var buf bytes.Buffer
 		archive := zip.NewWriter(&buf)
-		w, _ := archive.Create(name)
-		w.Write([]byte(content))
+		for name, content := range files {
+			w, _ := archive.Create(name)
+			w.Write([]byte(content))
+		}
 		archive.Close()
 		return buf.Bytes()
 	}
 
 	t.Run("MissingNuspecFile", func(t *testing.T) {
-		data := createArchive("dummy.txt", "")
+		data := createArchive(map[string]string{"dummy.txt": ""})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
@@ -78,7 +81,7 @@ func TestParsePackageMetaData(t *testing.T) {
 	})
 
 	t.Run("MissingNuspecFileInRoot", func(t *testing.T) {
-		data := createArchive("sub/package.nuspec", "")
+		data := createArchive(map[string]string{"sub/package.nuspec": ""})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
@@ -86,7 +89,7 @@ func TestParsePackageMetaData(t *testing.T) {
 	})
 
 	t.Run("InvalidNuspecFile", func(t *testing.T) {
-		data := createArchive("package.nuspec", "")
+		data := createArchive(map[string]string{"package.nuspec": ""})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
@@ -94,10 +97,10 @@ func TestParsePackageMetaData(t *testing.T) {
 	})
 
 	t.Run("InvalidPackageId", func(t *testing.T) {
-		data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?>
+		data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
 		<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
 		  <metadata></metadata>
-		</package>`)
+		</package>`})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
@@ -105,30 +108,34 @@ func TestParsePackageMetaData(t *testing.T) {
 	})
 
 	t.Run("InvalidPackageVersion", func(t *testing.T) {
-		data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?>
+		data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
 		<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
 		  <metadata>
-			<id>`+id+`</id>
+			<id>` + id + `</id>
 		  </metadata>
-		</package>`)
+		</package>`})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
 		assert.ErrorIs(t, err, ErrNuspecInvalidVersion)
 	})
 
-	t.Run("Valid", func(t *testing.T) {
-		data := createArchive("package.nuspec", nuspecContent)
+	t.Run("MissingReadme", func(t *testing.T) {
+		data := createArchive(map[string]string{"package.nuspec": nuspecContent})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.NoError(t, err)
 		assert.NotNil(t, np)
+		assert.Empty(t, np.Metadata.Readme)
 	})
-}
 
-func TestParseNuspecMetaData(t *testing.T) {
 	t.Run("Dependency Package", func(t *testing.T) {
-		np, err := ParseNuspecMetaData(strings.NewReader(nuspecContent))
+		data := createArchive(map[string]string{
+			"package.nuspec": nuspecContent,
+			"README.md":      readme,
+		})
+
+		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.NoError(t, err)
 		assert.NotNil(t, np)
 		assert.Equal(t, DependencyPackage, np.PackageType)
@@ -139,6 +146,7 @@ func TestParseNuspecMetaData(t *testing.T) {
 		assert.Equal(t, projectURL, np.Metadata.ProjectURL)
 		assert.Equal(t, description, np.Metadata.Description)
 		assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes)
+		assert.Equal(t, readme, np.Metadata.Readme)
 		assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL)
 		assert.Len(t, np.Metadata.Dependencies, 1)
 		assert.Contains(t, np.Metadata.Dependencies, targetFramework)
@@ -148,13 +156,15 @@ func TestParseNuspecMetaData(t *testing.T) {
 		assert.Equal(t, dependencyVersion, deps[0].Version)
 
 		t.Run("NormalizedVersion", func(t *testing.T) {
-			np, err := ParseNuspecMetaData(strings.NewReader(`<?xml version="1.0" encoding="utf-8"?>
-<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
-  <metadata>
-	<id>test</id>
-	<version>1.04.5.2.5-rc.1+metadata</version>
-  </metadata>
-</package>`))
+			data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
+				<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
+				  <metadata>
+					<id>test</id>
+					<version>1.04.5.2.5-rc.1+metadata</version>
+				  </metadata>
+				</package>`})
+
+			np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 			assert.NoError(t, err)
 			assert.NotNil(t, np)
 			assert.Equal(t, "1.4.5.2-rc.1", np.Version)
@@ -162,7 +172,9 @@ func TestParseNuspecMetaData(t *testing.T) {
 	})
 
 	t.Run("Symbols Package", func(t *testing.T) {
-		np, err := ParseNuspecMetaData(strings.NewReader(symbolsNuspecContent))
+		data := createArchive(map[string]string{"package.nuspec": symbolsNuspecContent})
+
+		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.NoError(t, err)
 		assert.NotNil(t, np)
 		assert.Equal(t, SymbolsPackage, np.PackageType)
diff --git a/modules/private/hook.go b/modules/private/hook.go
index cab8c81224..79c3d48229 100644
--- a/modules/private/hook.go
+++ b/modules/private/hook.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 )
 
@@ -32,13 +33,13 @@ const (
 )
 
 // Bool checks for a key in the map and parses as a boolean
-func (g GitPushOptions) Bool(key string, def bool) bool {
+func (g GitPushOptions) Bool(key string) optional.Option[bool] {
 	if val, ok := g[key]; ok {
 		if b, err := strconv.ParseBool(val); err == nil {
-			return b
+			return optional.Some(b)
 		}
 	}
-	return def
+	return optional.None[bool]()
 }
 
 // HookOptions represents the options for the Hook calls
@@ -87,13 +88,17 @@ type HookProcReceiveResult struct {
 
 // HookProcReceiveRefResult represents an individual result from ProcReceive
 type HookProcReceiveRefResult struct {
-	OldOID       string
-	NewOID       string
-	Ref          string
-	OriginalRef  git.RefName
-	IsForcePush  bool
-	IsNotMatched bool
-	Err          string
+	OldOID            string
+	NewOID            string
+	Ref               string
+	OriginalRef       git.RefName
+	IsForcePush       bool
+	IsNotMatched      bool
+	Err               string
+	IsCreatePR        bool
+	URL               string
+	ShouldShowMessage bool
+	HeadBranch        string
 }
 
 // HookPreReceive check whether the provided commits are allowed
diff --git a/modules/queue/workergroup.go b/modules/queue/workergroup.go
index 147a4f335e..e3801ef2b2 100644
--- a/modules/queue/workergroup.go
+++ b/modules/queue/workergroup.go
@@ -60,6 +60,9 @@ func (q *WorkerPoolQueue[T]) doDispatchBatchToWorker(wg *workerGroup[T], flushCh
 		full = true
 	}
 
+	// TODO: the logic could be improved in the future, to avoid a data-race between "doStartNewWorker" and "workerNum"
+	// The root problem is that if we skip "doStartNewWorker" here, the "workerNum" might be decreased by other workers later
+	// So ideally, it should check whether there are enough workers by some approaches, and start new workers if necessary.
 	q.workerNumMu.Lock()
 	noWorker := q.workerNum == 0
 	if full || noWorker {
@@ -143,7 +146,11 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
 		log.Debug("Queue %q starts new worker", q.GetName())
 		defer log.Debug("Queue %q stops idle worker", q.GetName())
 
+		atomic.AddInt32(&q.workerStartedCounter, 1) // Only increase counter, used for debugging
+
 		t := time.NewTicker(workerIdleDuration)
+		defer t.Stop()
+
 		keepWorking := true
 		stopWorking := func() {
 			q.workerNumMu.Lock()
@@ -158,13 +165,18 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
 			case batch, ok := <-q.batchChan:
 				if !ok {
 					stopWorking()
-				} else {
-					q.doWorkerHandle(batch)
-					t.Reset(workerIdleDuration)
+					continue
+				}
+				q.doWorkerHandle(batch)
+				// reset the idle ticker, and drain the tick after reset in case a tick is already triggered
+				t.Reset(workerIdleDuration)
+				select {
+				case <-t.C:
+				default:
 				}
 			case <-t.C:
 				q.workerNumMu.Lock()
-				keepWorking = q.workerNum <= 1
+				keepWorking = q.workerNum <= 1 // keep the last worker running
 				if !keepWorking {
 					q.workerNum--
 				}
diff --git a/modules/queue/workerqueue.go b/modules/queue/workerqueue.go
index b28fd88027..4160622d81 100644
--- a/modules/queue/workerqueue.go
+++ b/modules/queue/workerqueue.go
@@ -40,6 +40,8 @@ type WorkerPoolQueue[T any] struct {
 	workerMaxNum    int
 	workerActiveNum int
 	workerNumMu     sync.Mutex
+
+	workerStartedCounter int32
 }
 
 type flushType chan struct{}
diff --git a/modules/queue/workerqueue_test.go b/modules/queue/workerqueue_test.go
index e60120162a..e09669c542 100644
--- a/modules/queue/workerqueue_test.go
+++ b/modules/queue/workerqueue_test.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -175,11 +176,7 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett
 }
 
 func TestWorkerPoolQueueActiveWorkers(t *testing.T) {
-	oldWorkerIdleDuration := workerIdleDuration
-	workerIdleDuration = 300 * time.Millisecond
-	defer func() {
-		workerIdleDuration = oldWorkerIdleDuration
-	}()
+	defer test.MockVariableValue(&workerIdleDuration, 300*time.Millisecond)()
 
 	handler := func(items ...int) (unhandled []int) {
 		time.Sleep(100 * time.Millisecond)
@@ -250,3 +247,25 @@ func TestWorkerPoolQueueShutdown(t *testing.T) {
 	q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", qs, handler, false)
 	assert.EqualValues(t, 20, q.GetQueueItemNumber())
 }
+
+func TestWorkerPoolQueueWorkerIdleReset(t *testing.T) {
+	defer test.MockVariableValue(&workerIdleDuration, 10*time.Millisecond)()
+
+	handler := func(items ...int) (unhandled []int) {
+		time.Sleep(50 * time.Millisecond)
+		return nil
+	}
+
+	q, _ := newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 2, Length: 100}, handler, false)
+	stop := runWorkerPoolQueue(q)
+	for i := 0; i < 20; i++ {
+		assert.NoError(t, q.Push(i))
+	}
+
+	time.Sleep(500 * time.Millisecond)
+	assert.EqualValues(t, 2, q.GetWorkerNumber())
+	assert.EqualValues(t, 2, q.GetWorkerActiveNumber())
+	// when the queue never becomes empty, the existing workers should keep working
+	assert.EqualValues(t, 2, q.workerStartedCounter)
+	stop()
+}
diff --git a/modules/references/references.go b/modules/references/references.go
index 7758312564..761d6ee3d1 100644
--- a/modules/references/references.go
+++ b/modules/references/references.go
@@ -31,9 +31,9 @@ var (
 	// mentionPattern matches all mentions in the form of "@user" or "@org/team"
 	mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_]+\/?[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+\/?[0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`)
 	// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
-	issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\')([#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
+	issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\")([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$)`)
 	// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
-	issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`)
+	issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\')`)
 	// crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
 	// e.g. org/repo#12345
 	crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
diff --git a/modules/references/references_test.go b/modules/references/references_test.go
index ba7dda80cc..0c32933619 100644
--- a/modules/references/references_test.go
+++ b/modules/references/references_test.go
@@ -429,6 +429,8 @@ func TestRegExp_issueNumericPattern(t *testing.T) {
 		"  #12",
 		"#12:",
 		"ref: #12: msg",
+		"\"#1234\"",
+		"'#1234'",
 	}
 	falseTestCases := []string{
 		"# 1234",
@@ -459,6 +461,8 @@ func TestRegExp_issueAlphanumericPattern(t *testing.T) {
 		"(ABC-123)",
 		"[ABC-123]",
 		"ABC-123:",
+		"\"ABC-123\"",
+		"'ABC-123'",
 	}
 	falseTestCases := []string{
 		"RC-08",
diff --git a/modules/repository/collaborator.go b/modules/repository/collaborator.go
index ebe14e3a4c..f71c58fbdf 100644
--- a/modules/repository/collaborator.go
+++ b/modules/repository/collaborator.go
@@ -16,6 +16,14 @@ import (
 )
 
 func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error {
+	if err := repo.LoadOwner(ctx); err != nil {
+		return err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) {
+		return user_model.ErrBlockedUser
+	}
+
 	return db.WithTx(ctx, func(ctx context.Context) error {
 		has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{
 			"repo_id": repo.ID,
diff --git a/modules/repository/create.go b/modules/repository/create.go
index 7c954a1412..4f18b9b3fa 100644
--- a/modules/repository/create.go
+++ b/modules/repository/create.go
@@ -87,7 +87,17 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
 			units = append(units, repo_model.RepoUnit{
 				RepoID: repo.ID,
 				Type:   tp,
-				Config: &repo_model.PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), AllowRebaseUpdate: true},
+				Config: &repo_model.PullRequestsConfig{
+					AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true,
+					DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle),
+					AllowRebaseUpdate: true,
+				},
+			})
+		} else if tp == unit.TypeProjects {
+			units = append(units, repo_model.RepoUnit{
+				RepoID: repo.ID,
+				Type:   tp,
+				Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll},
 			})
 		} else {
 			units = append(units, repo_model.RepoUnit{
@@ -143,7 +153,7 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
 	}
 
 	if setting.Service.AutoWatchNewRepos {
-		if err = repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
+		if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
 			return fmt.Errorf("WatchRepo: %w", err)
 		}
 	}
diff --git a/modules/repository/init.go b/modules/repository/init.go
index 9c8e003bcb..57141d5ec7 100644
--- a/modules/repository/init.go
+++ b/modules/repository/init.go
@@ -6,15 +6,12 @@ package repository
 import (
 	"context"
 	"fmt"
-	"os"
 	"path/filepath"
 	"sort"
 	"strings"
-	"time"
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
-	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/label"
@@ -22,7 +19,6 @@ import (
 	"code.gitea.io/gitea/modules/options"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
-	asymkey_service "code.gitea.io/gitea/services/asymkey"
 )
 
 type OptionFile struct {
@@ -125,70 +121,6 @@ func LoadRepoConfig() error {
 	return nil
 }
 
-// InitRepoCommit temporarily changes with work directory.
-func InitRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) {
-	commitTimeStr := time.Now().Format(time.RFC3339)
-
-	sig := u.NewGitSig()
-	// Because this may call hooks we should pass in the environment
-	env := append(os.Environ(),
-		"GIT_AUTHOR_NAME="+sig.Name,
-		"GIT_AUTHOR_EMAIL="+sig.Email,
-		"GIT_AUTHOR_DATE="+commitTimeStr,
-		"GIT_COMMITTER_DATE="+commitTimeStr,
-	)
-	committerName := sig.Name
-	committerEmail := sig.Email
-
-	if stdout, _, err := git.NewCommand(ctx, "add", "--all").
-		SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)).
-		RunStdString(&git.RunOpts{Dir: tmpPath}); err != nil {
-		log.Error("git add --all failed: Stdout: %s\nError: %v", stdout, err)
-		return fmt.Errorf("git add --all: %w", err)
-	}
-
-	cmd := git.NewCommand(ctx, "commit", "--message=Initial commit").
-		AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
-
-	sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)
-	if sign {
-		cmd.AddOptionFormat("-S%s", keyID)
-
-		if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
-			// need to set the committer to the KeyID owner
-			committerName = signer.Name
-			committerEmail = signer.Email
-		}
-	} else {
-		cmd.AddArguments("--no-gpg-sign")
-	}
-
-	env = append(env,
-		"GIT_COMMITTER_NAME="+committerName,
-		"GIT_COMMITTER_EMAIL="+committerEmail,
-	)
-
-	if stdout, _, err := cmd.
-		SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)).
-		RunStdString(&git.RunOpts{Dir: tmpPath, Env: env}); err != nil {
-		log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.String(), stdout, err)
-		return fmt.Errorf("git commit: %w", err)
-	}
-
-	if len(defaultBranch) == 0 {
-		defaultBranch = setting.Repository.DefaultBranch
-	}
-
-	if stdout, _, err := git.NewCommand(ctx, "push", "origin").AddDynamicArguments("HEAD:" + defaultBranch).
-		SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)).
-		RunStdString(&git.RunOpts{Dir: tmpPath, Env: InternalPushingEnvironment(u, repo)}); err != nil {
-		log.Error("Failed to push back to HEAD: Stdout: %s\nError: %v", stdout, err)
-		return fmt.Errorf("git push: %w", err)
-	}
-
-	return nil
-}
-
 func CheckInitRepository(ctx context.Context, repo *repo_model.Repository, objectFormatName string) (err error) {
 	// Somehow the directory could exist.
 	isExist, err := gitrepo.IsRepositoryExist(ctx, repo)
diff --git a/modules/repository/repo.go b/modules/repository/repo.go
index f9ea3ddc11..cb926084ba 100644
--- a/modules/repository/repo.go
+++ b/modules/repository/repo.go
@@ -5,16 +5,13 @@ package repository
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"io"
-	"net/http"
 	"strings"
 	"time"
 
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
-	"code.gitea.io/gitea/models/organization"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
@@ -22,10 +19,8 @@ import (
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/migration"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 )
 
 /*
@@ -47,244 +42,6 @@ func WikiRemoteURL(ctx context.Context, remote string) string {
 	return ""
 }
 
-// MigrateRepositoryGitData starts migrating git related data after created migrating repository
-func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
-	repo *repo_model.Repository, opts migration.MigrateOptions,
-	httpTransport *http.Transport,
-) (*repo_model.Repository, error) {
-	repoPath := repo_model.RepoPath(u.Name, opts.RepoName)
-
-	if u.IsOrganization() {
-		t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
-		if err != nil {
-			return nil, err
-		}
-		repo.NumWatches = t.NumMembers
-	} else {
-		repo.NumWatches = 1
-	}
-
-	migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second
-
-	var err error
-	if err = util.RemoveAll(repoPath); err != nil {
-		return repo, fmt.Errorf("Failed to remove %s: %w", repoPath, err)
-	}
-
-	if err = git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{
-		Mirror:        true,
-		Quiet:         true,
-		Timeout:       migrateTimeout,
-		SkipTLSVerify: setting.Migrations.SkipTLSVerify,
-	}); err != nil {
-		if errors.Is(err, context.DeadlineExceeded) {
-			return repo, fmt.Errorf("Clone timed out. Consider increasing [git.timeout] MIGRATE in app.ini. Underlying Error: %w", err)
-		}
-		return repo, fmt.Errorf("Clone: %w", err)
-	}
-
-	if err := git.WriteCommitGraph(ctx, repoPath); err != nil {
-		return repo, err
-	}
-
-	if opts.Wiki {
-		wikiPath := repo_model.WikiPath(u.Name, opts.RepoName)
-		wikiRemotePath := WikiRemoteURL(ctx, opts.CloneAddr)
-		if len(wikiRemotePath) > 0 {
-			if err := util.RemoveAll(wikiPath); err != nil {
-				return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
-			}
-
-			if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
-				Mirror:        true,
-				Quiet:         true,
-				Timeout:       migrateTimeout,
-				Branch:        "master",
-				SkipTLSVerify: setting.Migrations.SkipTLSVerify,
-			}); err != nil {
-				log.Warn("Clone wiki: %v", err)
-				if err := util.RemoveAll(wikiPath); err != nil {
-					return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
-				}
-			} else {
-				if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
-					return repo, err
-				}
-			}
-		}
-	}
-
-	if repo.OwnerID == u.ID {
-		repo.Owner = u
-	}
-
-	if err = CheckDaemonExportOK(ctx, repo); err != nil {
-		return repo, fmt.Errorf("checkDaemonExportOK: %w", err)
-	}
-
-	if stdout, _, err := git.NewCommand(ctx, "update-server-info").
-		SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)).
-		RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
-		log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
-		return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err)
-	}
-
-	gitRepo, err := git.OpenRepository(ctx, repoPath)
-	if err != nil {
-		return repo, fmt.Errorf("OpenRepository: %w", err)
-	}
-	defer gitRepo.Close()
-
-	repo.IsEmpty, err = gitRepo.IsEmpty()
-	if err != nil {
-		return repo, fmt.Errorf("git.IsEmpty: %w", err)
-	}
-
-	if !repo.IsEmpty {
-		if len(repo.DefaultBranch) == 0 {
-			// Try to get HEAD branch and set it as default branch.
-			headBranch, err := gitRepo.GetHEADBranch()
-			if err != nil {
-				return repo, fmt.Errorf("GetHEADBranch: %w", err)
-			}
-			if headBranch != nil {
-				repo.DefaultBranch = headBranch.Name
-			}
-		}
-
-		if _, err := SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil {
-			return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err)
-		}
-
-		if !opts.Releases {
-			// note: this will greatly improve release (tag) sync
-			// for pull-mirrors with many tags
-			repo.IsMirror = opts.Mirror
-			if err = SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
-				log.Error("Failed to synchronize tags to releases for repository: %v", err)
-			}
-		}
-
-		if opts.LFS {
-			endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint)
-			lfsClient := lfs.NewClient(endpoint, httpTransport)
-			if err = StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil {
-				log.Error("Failed to store missing LFS objects for repository: %v", err)
-			}
-		}
-	}
-
-	ctx, committer, err := db.TxContext(ctx)
-	if err != nil {
-		return nil, err
-	}
-	defer committer.Close()
-
-	if opts.Mirror {
-		remoteAddress, err := util.SanitizeURL(opts.CloneAddr)
-		if err != nil {
-			return repo, err
-		}
-		mirrorModel := repo_model.Mirror{
-			RepoID:         repo.ID,
-			Interval:       setting.Mirror.DefaultInterval,
-			EnablePrune:    true,
-			NextUpdateUnix: timeutil.TimeStampNow().AddDuration(setting.Mirror.DefaultInterval),
-			LFS:            opts.LFS,
-			RemoteAddress:  remoteAddress,
-		}
-		if opts.LFS {
-			mirrorModel.LFSEndpoint = opts.LFSEndpoint
-		}
-
-		if opts.MirrorInterval != "" {
-			parsedInterval, err := time.ParseDuration(opts.MirrorInterval)
-			if err != nil {
-				log.Error("Failed to set Interval: %v", err)
-				return repo, err
-			}
-			if parsedInterval == 0 {
-				mirrorModel.Interval = 0
-				mirrorModel.NextUpdateUnix = 0
-			} else if parsedInterval < setting.Mirror.MinInterval {
-				err := fmt.Errorf("interval %s is set below Minimum Interval of %s", parsedInterval, setting.Mirror.MinInterval)
-				log.Error("Interval: %s is too frequent", opts.MirrorInterval)
-				return repo, err
-			} else {
-				mirrorModel.Interval = parsedInterval
-				mirrorModel.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(parsedInterval)
-			}
-		}
-
-		if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil {
-			return repo, fmt.Errorf("InsertOne: %w", err)
-		}
-
-		repo.IsMirror = true
-		if err = UpdateRepository(ctx, repo, false); err != nil {
-			return nil, err
-		}
-
-		// this is necessary for sync local tags from remote
-		configName := fmt.Sprintf("remote.%s.fetch", mirrorModel.GetRemoteName())
-		if stdout, _, err := git.NewCommand(ctx, "config").
-			AddOptionValues("--add", configName, `+refs/tags/*:refs/tags/*`).
-			RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
-			log.Error("MigrateRepositoryGitData(git config --add <remote> +refs/tags/*:refs/tags/*) in %v: Stdout: %s\nError: %v", repo, stdout, err)
-			return repo, fmt.Errorf("error in MigrateRepositoryGitData(git config --add <remote> +refs/tags/*:refs/tags/*): %w", err)
-		}
-	} else {
-		if err = UpdateRepoSize(ctx, repo); err != nil {
-			log.Error("Failed to update size for repository: %v", err)
-		}
-		if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil {
-			return nil, err
-		}
-	}
-
-	return repo, committer.Commit()
-}
-
-// cleanUpMigrateGitConfig removes mirror info which prevents "push --all".
-// This also removes possible user credentials.
-func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error {
-	cmd := git.NewCommand(ctx, "remote", "rm", "origin")
-	// if the origin does not exist
-	_, stderr, err := cmd.RunStdString(&git.RunOpts{
-		Dir: repoPath,
-	})
-	if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") {
-		return err
-	}
-	return nil
-}
-
-// CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors.
-func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) {
-	repoPath := repo.RepoPath()
-	if err := gitrepo.CreateDelegateHooks(ctx, repo, false); err != nil {
-		return repo, fmt.Errorf("createDelegateHooks: %w", err)
-	}
-	if repo.HasWiki() {
-		if err := gitrepo.CreateDelegateHooks(ctx, repo, true); err != nil {
-			return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err)
-		}
-	}
-
-	_, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
-	if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
-		return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err)
-	}
-
-	if repo.HasWiki() {
-		if err := cleanUpMigrateGitConfig(ctx, repo.WikiPath()); err != nil {
-			return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err)
-		}
-	}
-
-	return repo, UpdateRepository(ctx, repo, false)
-}
-
 // SyncRepoTags synchronizes releases table with repository tags
 func SyncRepoTags(ctx context.Context, repoID int64) error {
 	repo, err := repo_model.GetRepositoryByID(ctx, repoID)
@@ -352,7 +109,9 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR
 		}
 
 		if err := PushUpdateAddTag(ctx, repo, gitRepo, tagName, sha1, refname); err != nil {
-			return fmt.Errorf("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %w", tagName, repo.ID, repo.OwnerName, repo.Name, err)
+			// sometimes, some tags will be sync failed. i.e. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tag/?h=v2.6.11
+			// this is a tree object, not a tag object which created before git
+			log.Error("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %v", tagName, repo.ID, repo.OwnerName, repo.Name, err)
 		}
 
 		return nil
diff --git a/modules/secret/secret.go b/modules/secret/secret.go
index 9c2ecd181d..e70ae1839c 100644
--- a/modules/secret/secret.go
+++ b/modules/secret/secret.go
@@ -7,13 +7,12 @@ import (
 	"crypto/aes"
 	"crypto/cipher"
 	"crypto/rand"
+	"crypto/sha256"
 	"encoding/base64"
 	"encoding/hex"
 	"errors"
 	"fmt"
 	"io"
-
-	"github.com/minio/sha256-simd"
 )
 
 // AesEncrypt encrypts text and given key with AES.
diff --git a/modules/setting/admin.go b/modules/setting/admin.go
index 2d2dd26de9..8aebc76154 100644
--- a/modules/setting/admin.go
+++ b/modules/setting/admin.go
@@ -3,14 +3,28 @@
 
 package setting
 
+import (
+	"code.gitea.io/gitea/modules/container"
+)
+
 // Admin settings
 var Admin struct {
-	DisableRegularOrgCreation bool
-	DefaultEmailNotification  string
+	DisableRegularOrgCreation   bool
+	DefaultEmailNotification    string
+	UserDisabledFeatures        container.Set[string]
+	ExternalUserDisableFeatures container.Set[string]
 }
 
 func loadAdminFrom(rootCfg ConfigProvider) {
-	mustMapSetting(rootCfg, "admin", &Admin)
 	sec := rootCfg.Section("admin")
+	Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
 	Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
+	Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...)
+	Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...)
 }
+
+const (
+	UserFeatureDeletion      = "deletion"
+	UserFeatureManageSSHKeys = "manage_ssh_keys"
+	UserFeatureManageGPGKeys = "manage_gpg_keys"
+)
diff --git a/modules/setting/attachment.go b/modules/setting/attachment.go
index 934d4d7f46..0fdabb5032 100644
--- a/modules/setting/attachment.go
+++ b/modules/setting/attachment.go
@@ -12,7 +12,7 @@ var Attachment = struct {
 	Enabled      bool
 }{
 	Storage:      &Storage{},
-	AllowedTypes: ".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip",
+	AllowedTypes: ".cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip",
 	MaxSize:      2048,
 	MaxFiles:     5,
 	Enabled:      true,
@@ -25,7 +25,7 @@ func loadAttachmentFrom(rootCfg ConfigProvider) (err error) {
 		return err
 	}
 
-	Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip")
+	Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip")
 	Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(2048)
 	Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(5)
 	Attachment.Enabled = sec.Key("ENABLED").MustBool(true)
diff --git a/modules/setting/config.go b/modules/setting/config.go
index db189f44ac..03558574c2 100644
--- a/modules/setting/config.go
+++ b/modules/setting/config.go
@@ -15,8 +15,45 @@ type PictureStruct struct {
 	EnableFederatedAvatar *config.Value[bool]
 }
 
+type OpenWithEditorApp struct {
+	DisplayName string
+	OpenURL     string
+}
+
+type OpenWithEditorAppsType []OpenWithEditorApp
+
+func (t OpenWithEditorAppsType) ToTextareaString() string {
+	ret := ""
+	for _, app := range t {
+		ret += app.DisplayName + " = " + app.OpenURL + "\n"
+	}
+	return ret
+}
+
+func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
+	return OpenWithEditorAppsType{
+		{
+			DisplayName: "VS Code",
+			OpenURL:     "vscode://vscode.git/clone?url={url}",
+		},
+		{
+			DisplayName: "VSCodium",
+			OpenURL:     "vscodium://vscode.git/clone?url={url}",
+		},
+		{
+			DisplayName: "Intellij IDEA",
+			OpenURL:     "jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo={url}",
+		},
+	}
+}
+
+type RepositoryStruct struct {
+	OpenWithEditorApps *config.Value[OpenWithEditorAppsType]
+}
+
 type ConfigStruct struct {
-	Picture *PictureStruct
+	Picture    *PictureStruct
+	Repository *RepositoryStruct
 }
 
 var (
@@ -28,8 +65,11 @@ func initDefaultConfig() {
 	config.SetCfgSecKeyGetter(&cfgSecKeyGetter{})
 	defaultConfig = &ConfigStruct{
 		Picture: &PictureStruct{
-			DisableGravatar:       config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}, "picture.disable_gravatar"),
-			EnableFederatedAvatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}, "picture.enable_federated_avatar"),
+			DisableGravatar:       config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}),
+			EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}),
+		},
+		Repository: &RepositoryStruct{
+			OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
 		},
 	}
 }
@@ -42,6 +82,9 @@ func Config() *ConfigStruct {
 type cfgSecKeyGetter struct{}
 
 func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) {
+	if key == "" {
+		return "", false
+	}
 	cfgSec, err := CfgProvider.GetSection(sec)
 	if err != nil {
 		log.Error("Unable to get config section: %q", sec)
diff --git a/modules/setting/config/value.go b/modules/setting/config/value.go
index 817fcdb786..f0ec120544 100644
--- a/modules/setting/config/value.go
+++ b/modules/setting/config/value.go
@@ -5,8 +5,11 @@ package config
 
 import (
 	"context"
-	"strconv"
 	"sync"
+
+	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
 )
 
 type CfgSecKey struct {
@@ -23,14 +26,14 @@ type Value[T any] struct {
 	revision   int
 }
 
-func (value *Value[T]) parse(s string) (v T) {
-	switch any(v).(type) {
-	case bool:
-		b, _ := strconv.ParseBool(s)
-		return any(b).(T)
-	default:
-		panic("unsupported config type, please complete the code")
+func (value *Value[T]) parse(key, valStr string) (v T) {
+	v = value.def
+	if valStr != "" {
+		if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil {
+			log.Error("Unable to unmarshal json config for key %q, err: %v", key, err)
+		}
 	}
+	return v
 }
 
 func (value *Value[T]) Value(ctx context.Context) (v T) {
@@ -62,7 +65,7 @@ func (value *Value[T]) Value(ctx context.Context) (v T) {
 	if valStr == nil {
 		v = value.def
 	} else {
-		v = value.parse(*valStr)
+		v = value.parse(value.dynKey, *valStr)
 	}
 
 	value.mu.Lock()
@@ -76,6 +79,16 @@ func (value *Value[T]) DynKey() string {
 	return value.dynKey
 }
 
-func Bool(def bool, cfgSecKey CfgSecKey, dynKey string) *Value[bool] {
-	return &Value[bool]{def: def, cfgSecKey: cfgSecKey, dynKey: dynKey}
+func (value *Value[T]) WithDefault(def T) *Value[T] {
+	value.def = def
+	return value
+}
+
+func (value *Value[T]) WithFileConfig(cfgSecKey CfgSecKey) *Value[T] {
+	value.cfgSecKey = cfgSecKey
+	return value
+}
+
+func ValueJSON[T any](dynKey string) *Value[T] {
+	return &Value[T]{dynKey: dynKey}
 }
diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go
index 132f4acea1..03f27ba203 100644
--- a/modules/setting/config_provider.go
+++ b/modules/setting/config_provider.go
@@ -196,7 +196,7 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
 
 // NewConfigProviderFromFile load configuration from file.
 // NOTE: do not print any log except error.
-func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) {
+func NewConfigProviderFromFile(file string) (ConfigProvider, error) {
 	cfg := ini.Empty(configProviderLoadOptions())
 	loadedFromEmpty := true
 
@@ -213,12 +213,6 @@ func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvi
 		}
 	}
 
-	for _, s := range extraConfigs {
-		if err := cfg.Append([]byte(s)); err != nil {
-			return nil, fmt.Errorf("unable to append more config: %v", err)
-		}
-	}
-
 	cfg.NameMapper = ini.SnackCase
 	return &iniConfigProvider{
 		file:            file,
@@ -321,21 +315,25 @@ func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) {
 	}
 }
 
-// DeprecatedWarnings contains the warning message for various deprecations, including: setting option, file/folder, etc
-var DeprecatedWarnings []string
+// StartupProblems contains the messages for various startup problems, including: setting option, file/folder, etc
+var StartupProblems []string
+
+func logStartupProblem(skip int, level log.Level, format string, args ...any) {
+	msg := fmt.Sprintf(format, args...)
+	log.Log(skip+1, level, "%s", msg)
+	StartupProblems = append(StartupProblems, msg)
+}
 
 func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
 	if rootCfg.Section(oldSection).HasKey(oldKey) {
-		msg := fmt.Sprintf("Deprecated config option `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
-		log.Error("%v", msg)
-		DeprecatedWarnings = append(DeprecatedWarnings, msg)
+		logStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
 	}
 }
 
 // deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
 func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
 	if rootCfg.Section(oldSection).HasKey(oldKey) {
-		log.Error("Deprecated `[%s]` `%s` present which has been copied to database table sys_setting", oldSection, oldKey)
+		logStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
 	}
 }
 
diff --git a/modules/setting/database.go b/modules/setting/database.go
index e200b15b2e..1a4bf64805 100644
--- a/modules/setting/database.go
+++ b/modules/setting/database.go
@@ -25,26 +25,27 @@ var (
 
 	// Database holds the database settings
 	Database = struct {
-		Type              DatabaseType
-		Host              string
-		Name              string
-		User              string
-		Passwd            string
-		Schema            string
-		SSLMode           string
-		Path              string
-		LogSQL            bool
-		MysqlCharset      string
-		CharsetCollation  string
-		Timeout           int // seconds
-		SQLiteJournalMode string
-		DBConnectRetries  int
-		DBConnectBackoff  time.Duration
-		MaxIdleConns      int
-		MaxOpenConns      int
-		ConnMaxLifetime   time.Duration
-		IterateBufferSize int
-		AutoMigration     bool
+		Type               DatabaseType
+		Host               string
+		Name               string
+		User               string
+		Passwd             string
+		Schema             string
+		SSLMode            string
+		Path               string
+		LogSQL             bool
+		MysqlCharset       string
+		CharsetCollation   string
+		Timeout            int // seconds
+		SQLiteJournalMode  string
+		DBConnectRetries   int
+		DBConnectBackoff   time.Duration
+		MaxIdleConns       int
+		MaxOpenConns       int
+		ConnMaxLifetime    time.Duration
+		IterateBufferSize  int
+		AutoMigration      bool
+		SlowQueryThreshold time.Duration
 	}{
 		Timeout:           500,
 		IterateBufferSize: 50,
@@ -87,6 +88,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
 	Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
 	Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
 	Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
+	Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(5 * time.Second)
 }
 
 // DBConnStr returns database connection string
diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go
index 16f3d80168..6877d70e3c 100644
--- a/modules/setting/indexer.go
+++ b/modules/setting/indexer.go
@@ -53,21 +53,24 @@ var Indexer = struct {
 func loadIndexerFrom(rootCfg ConfigProvider) {
 	sec := rootCfg.Section("indexer")
 	Indexer.IssueType = sec.Key("ISSUE_INDEXER_TYPE").MustString("bleve")
-	Indexer.IssuePath = filepath.ToSlash(sec.Key("ISSUE_INDEXER_PATH").MustString(filepath.ToSlash(filepath.Join(AppDataPath, "indexers/issues.bleve"))))
-	if !filepath.IsAbs(Indexer.IssuePath) {
-		Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
-	}
-	Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
-
-	if Indexer.IssueType == "meilisearch" {
-		u, err := url.Parse(Indexer.IssueConnStr)
-		if err != nil {
-			log.Warn("Failed to parse ISSUE_INDEXER_CONN_STR: %v", err)
-			u = &url.URL{}
+	if Indexer.IssueType == "bleve" {
+		Indexer.IssuePath = filepath.ToSlash(sec.Key("ISSUE_INDEXER_PATH").MustString(filepath.ToSlash(filepath.Join(AppDataPath, "indexers/issues.bleve"))))
+		if !filepath.IsAbs(Indexer.IssuePath) {
+			Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
+		}
+		checkOverlappedPath("[indexer].ISSUE_INDEXER_PATH", Indexer.IssuePath)
+	} else {
+		Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
+		if Indexer.IssueType == "meilisearch" {
+			u, err := url.Parse(Indexer.IssueConnStr)
+			if err != nil {
+				log.Warn("Failed to parse ISSUE_INDEXER_CONN_STR: %v", err)
+				u = &url.URL{}
+			}
+			Indexer.IssueConnAuth, _ = u.User.Password()
+			u.User = nil
+			Indexer.IssueConnStr = u.String()
 		}
-		Indexer.IssueConnAuth, _ = u.User.Password()
-		u.User = nil
-		Indexer.IssueConnStr = u.String()
 	}
 
 	Indexer.IssueIndexerName = sec.Key("ISSUE_INDEXER_NAME").MustString(Indexer.IssueIndexerName)
diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go
index a5ea537cef..2034ef782c 100644
--- a/modules/setting/lfs.go
+++ b/modules/setting/lfs.go
@@ -4,22 +4,19 @@
 package setting
 
 import (
-	"encoding/base64"
 	"fmt"
 	"time"
 
 	"code.gitea.io/gitea/modules/generate"
-	"code.gitea.io/gitea/modules/util"
 )
 
 // LFS represents the configuration for Git LFS
 var LFS = struct {
-	StartServer     bool          `ini:"LFS_START_SERVER"`
-	JWTSecretBase64 string        `ini:"LFS_JWT_SECRET"`
-	JWTSecretBytes  []byte        `ini:"-"`
-	HTTPAuthExpiry  time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
-	MaxFileSize     int64         `ini:"LFS_MAX_FILE_SIZE"`
-	LocksPagingNum  int           `ini:"LFS_LOCKS_PAGING_NUM"`
+	StartServer    bool          `ini:"LFS_START_SERVER"`
+	JWTSecretBytes []byte        `ini:"-"`
+	HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
+	MaxFileSize    int64         `ini:"LFS_MAX_FILE_SIZE"`
+	LocksPagingNum int           `ini:"LFS_LOCKS_PAGING_NUM"`
 
 	Storage *Storage
 }{}
@@ -61,10 +58,10 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
 		return nil
 	}
 
-	LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
-	LFS.JWTSecretBytes, err = util.Base64FixedDecode(base64.RawURLEncoding, []byte(LFS.JWTSecretBase64), 32)
+	jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
+	LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(jwtSecretBase64)
 	if err != nil {
-		LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64()
+		LFS.JWTSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
 		if err != nil {
 			return fmt.Errorf("error generating JWT Secret for custom config: %v", err)
 		}
@@ -74,8 +71,8 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
 		if err != nil {
 			return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
 		}
-		rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
-		saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
+		rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
+		saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
 		if err := saveCfg.Save(); err != nil {
 			return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
 		}
diff --git a/modules/setting/log.go b/modules/setting/log.go
index e404074b72..50c5779994 100644
--- a/modules/setting/log.go
+++ b/modules/setting/log.go
@@ -185,8 +185,13 @@ func InitLoggersForTest() {
 	initAllLoggers()
 }
 
+var initLoggerDisabled bool
+
 // initAllLoggers creates all the log services
 func initAllLoggers() {
+	if initLoggerDisabled {
+		return
+	}
 	initManagedLoggers(log.GetManager(), CfgProvider)
 
 	golog.SetFlags(0)
@@ -194,6 +199,10 @@ func initAllLoggers() {
 	golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
 }
 
+func DisableLoggerInit() {
+	initLoggerDisabled = true
+}
+
 func initManagedLoggers(manager *log.LoggerManager, cfg ConfigProvider) {
 	loadLogGlobalFrom(cfg)
 	prepareLoggerConfig(cfg)
diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go
index 0d15e91ef0..830472db32 100644
--- a/modules/setting/oauth2.go
+++ b/modules/setting/oauth2.go
@@ -4,13 +4,12 @@
 package setting
 
 import (
-	"encoding/base64"
 	"math"
 	"path/filepath"
+	"sync/atomic"
 
 	"code.gitea.io/gitea/modules/generate"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/util"
 )
 
 // OAuth2UsernameType is enum describing the way gitea 'name' should be generated from oauth2 data
@@ -98,7 +97,6 @@ var OAuth2 = struct {
 	RefreshTokenExpirationTime int64
 	InvalidateRefreshTokens    bool
 	JWTSigningAlgorithm        string `ini:"JWT_SIGNING_ALGORITHM"`
-	JWTSecretBase64            string `ini:"JWT_SECRET"`
 	JWTSigningPrivateKeyFile   string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
 	MaxTokenLength             int
 	DefaultApplications        []string
@@ -120,6 +118,10 @@ func loadOAuth2From(rootCfg ConfigProvider) {
 		return
 	}
 
+	if sec.HasKey("DEFAULT_APPLICATIONS") && sec.Key("DEFAULT_APPLICATIONS").String() == "" {
+		OAuth2.DefaultApplications = nil
+	}
+
 	// Handle the rename of ENABLE to ENABLED
 	deprecatedSetting(rootCfg, "oauth2", "ENABLE", "oauth2", "ENABLED", "v1.23.0")
 	if sec.HasKey("ENABLE") && !sec.HasKey("ENABLED") {
@@ -130,29 +132,50 @@ func loadOAuth2From(rootCfg ConfigProvider) {
 		return
 	}
 
-	OAuth2.JWTSecretBase64 = loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
+	jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
 
 	if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) {
 		OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
 	}
 
 	if InstallLock {
-		if _, err := util.Base64FixedDecode(base64.RawURLEncoding, []byte(OAuth2.JWTSecretBase64), 32); err != nil {
-			key, err := generate.NewJwtSecret()
+		jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
+		if err != nil {
+			jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
 			if err != nil {
 				log.Fatal("error generating JWT secret: %v", err)
 			}
-
-			OAuth2.JWTSecretBase64 = base64.RawURLEncoding.EncodeToString(key)
 			saveCfg, err := rootCfg.PrepareSaving()
 			if err != nil {
 				log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
 			}
-			rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
-			saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
+			rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
+			saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
 			if err := saveCfg.Save(); err != nil {
 				log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
 			}
 		}
+		generalSigningSecret.Store(&jwtSecretBytes)
 	}
 }
+
+// generalSigningSecret is used as container for a []byte value
+// instead of an additional mutex, we use CompareAndSwap func to change the value thread save
+var generalSigningSecret atomic.Pointer[[]byte]
+
+func GetGeneralTokenSigningSecret() []byte {
+	old := generalSigningSecret.Load()
+	if old == nil || len(*old) == 0 {
+		jwtSecret, _, err := generate.NewJwtSecretWithBase64()
+		if err != nil {
+			log.Fatal("Unable to generate general JWT secret: %s", err.Error())
+		}
+		if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
+			// FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
+			logStartupProblem(1, log.WARN, "OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes")
+			return jwtSecret
+		}
+		return *generalSigningSecret.Load()
+	}
+	return *old
+}
diff --git a/modules/setting/oauth2_test.go b/modules/setting/oauth2_test.go
new file mode 100644
index 0000000000..4403f35892
--- /dev/null
+++ b/modules/setting/oauth2_test.go
@@ -0,0 +1,52 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/modules/generate"
+	"code.gitea.io/gitea/modules/test"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetGeneralSigningSecret(t *testing.T) {
+	// when there is no general signing secret, it should be generated, and keep the same value
+	assert.Nil(t, generalSigningSecret.Load())
+	s1 := GetGeneralTokenSigningSecret()
+	assert.NotNil(t, s1)
+	s2 := GetGeneralTokenSigningSecret()
+	assert.Equal(t, s1, s2)
+
+	// the config value should always override any pre-generated value
+	cfg, _ := NewConfigProviderFromData(`
+[oauth2]
+JWT_SECRET = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+`)
+	defer test.MockVariableValue(&InstallLock, true)()
+	loadOAuth2From(cfg)
+	actual := GetGeneralTokenSigningSecret()
+	expected, _ := generate.DecodeJwtSecretBase64("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
+	assert.Len(t, actual, 32)
+	assert.EqualValues(t, expected, actual)
+}
+
+func TestOauth2DefaultApplications(t *testing.T) {
+	cfg, _ := NewConfigProviderFromData(``)
+	loadOAuth2From(cfg)
+	assert.Equal(t, []string{"git-credential-oauth", "git-credential-manager", "tea"}, OAuth2.DefaultApplications)
+
+	cfg, _ = NewConfigProviderFromData(`[oauth2]
+DEFAULT_APPLICATIONS = tea
+`)
+	loadOAuth2From(cfg)
+	assert.Equal(t, []string{"tea"}, OAuth2.DefaultApplications)
+
+	cfg, _ = NewConfigProviderFromData(`[oauth2]
+DEFAULT_APPLICATIONS =
+`)
+	loadOAuth2From(cfg)
+	assert.Nil(t, nil, OAuth2.DefaultApplications)
+}
diff --git a/modules/setting/other.go b/modules/setting/other.go
index 706cb1e3d9..4ba494765b 100644
--- a/modules/setting/other.go
+++ b/modules/setting/other.go
@@ -8,6 +8,7 @@ import "code.gitea.io/gitea/modules/log"
 type OtherConfig struct {
 	ShowFooterVersion          bool
 	ShowFooterTemplateLoadTime bool
+	ShowFooterPoweredBy        bool
 	EnableFeed                 bool
 	EnableSitemap              bool
 }
@@ -15,6 +16,7 @@ type OtherConfig struct {
 var Other = OtherConfig{
 	ShowFooterVersion:          true,
 	ShowFooterTemplateLoadTime: true,
+	ShowFooterPoweredBy:        true,
 	EnableSitemap:              true,
 	EnableFeed:                 true,
 }
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index a6f0ed8833..8656ebc7ec 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -285,6 +285,9 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
 	} else {
 		RepoRootPath = filepath.Clean(RepoRootPath)
 	}
+
+	checkOverlappedPath("[repository].ROOT", RepoRootPath)
+
 	defaultDetectedCharsetsOrder := make([]string, 0, len(Repository.DetectedCharsetsOrder))
 	for _, charset := range Repository.DetectedCharsetsOrder {
 		defaultDetectedCharsetsOrder = append(defaultDetectedCharsetsOrder, strings.ToLower(strings.TrimSpace(charset)))
diff --git a/modules/setting/security.go b/modules/setting/security.go
index 380360a696..3d7b1f9ce7 100644
--- a/modules/setting/security.go
+++ b/modules/setting/security.go
@@ -103,7 +103,7 @@ func generateSaveInternalToken(rootCfg ConfigProvider) {
 func loadSecurityFrom(rootCfg ConfigProvider) {
 	sec := rootCfg.Section("security")
 	InstallLock = HasInstallLock(rootCfg)
-	LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
+	LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(31)
 	SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
 	if SecretKey == "" {
 		// FIXME: https://github.com/go-gitea/gitea/issues/16832
diff --git a/modules/setting/server.go b/modules/setting/server.go
index c09b91612a..7d6ece2727 100644
--- a/modules/setting/server.go
+++ b/modules/setting/server.go
@@ -7,7 +7,6 @@ import (
 	"encoding/base64"
 	"net"
 	"net/url"
-	"path"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -321,17 +320,18 @@ func loadServerFrom(rootCfg ConfigProvider) {
 	}
 	StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(StaticRootPath)
 	StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour)
-	AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data"))
+	AppDataPath = sec.Key("APP_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data"))
 	if !filepath.IsAbs(AppDataPath) {
 		AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath))
 	}
 
 	EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
 	EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
-	PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(path.Join(AppWorkPath, "data/tmp/pprof"))
+	PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))
 	if !filepath.IsAbs(PprofDataPath) {
 		PprofDataPath = filepath.Join(AppWorkPath, PprofDataPath)
 	}
+	checkOverlappedPath("[server].PPROF_DATA_PATH", PprofDataPath)
 
 	landingPage := sec.Key("LANDING_PAGE").MustString("home")
 	switch landingPage {
diff --git a/modules/setting/session.go b/modules/setting/session.go
index 664c66f869..afe63bfdb7 100644
--- a/modules/setting/session.go
+++ b/modules/setting/session.go
@@ -5,7 +5,6 @@ package setting
 
 import (
 	"net/http"
-	"path"
 	"path/filepath"
 	"strings"
 
@@ -21,7 +20,7 @@ var SessionConfig = struct {
 	ProviderConfig string
 	// Cookie name to save session ID. Default is "MacaronSession".
 	CookieName string
-	// Cookie path to store. Default is "/". HINT: there was a bug, the old value doesn't have trailing slash, and could be empty "".
+	// Cookie path to store. Default is "/".
 	CookiePath string
 	// GC interval time in seconds. Default is 3600.
 	Gclifetime int64
@@ -44,12 +43,16 @@ func loadSessionFrom(rootCfg ConfigProvider) {
 	sec := rootCfg.Section("session")
 	SessionConfig.Provider = sec.Key("PROVIDER").In("memory",
 		[]string{"memory", "file", "redis", "mysql", "postgres", "couchbase", "memcache", "db"})
-	SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(path.Join(AppDataPath, "sessions")), "\" ")
+	SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(filepath.Join(AppDataPath, "sessions")), "\" ")
 	if SessionConfig.Provider == "file" && !filepath.IsAbs(SessionConfig.ProviderConfig) {
-		SessionConfig.ProviderConfig = path.Join(AppWorkPath, SessionConfig.ProviderConfig)
+		SessionConfig.ProviderConfig = filepath.Join(AppWorkPath, SessionConfig.ProviderConfig)
+		checkOverlappedPath("[session].PROVIDER_CONFIG", SessionConfig.ProviderConfig)
 	}
 	SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
-	SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash
+	SessionConfig.CookiePath = AppSubURL
+	if SessionConfig.CookiePath == "" {
+		SessionConfig.CookiePath = "/"
+	}
 	SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
 	SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
 	SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index d444d9a017..92bb0b6541 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -13,6 +13,7 @@ import (
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/user"
+	"code.gitea.io/gitea/modules/util"
 )
 
 // settings
@@ -90,9 +91,9 @@ func PrepareAppDataPath() error {
 	return nil
 }
 
-func InitCfgProvider(file string, extraConfigs ...string) {
+func InitCfgProvider(file string) {
 	var err error
-	if CfgProvider, err = NewConfigProviderFromFile(file, extraConfigs...); err != nil {
+	if CfgProvider, err = NewConfigProviderFromFile(file); err != nil {
 		log.Fatal("Unable to init config provider from %q: %v", file, err)
 	}
 	CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls
@@ -158,9 +159,11 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
 func loadRunModeFrom(rootCfg ConfigProvider) {
 	rootSec := rootCfg.Section("")
 	RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername())
+
 	// The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches.
 	// Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly.
 	unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")
+	unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || util.OptionalBoolParse(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value()
 	RunMode = os.Getenv("GITEA_RUN_MODE")
 	if RunMode == "" {
 		RunMode = rootSec.Key("RUN_MODE").MustString("prod")
@@ -226,3 +229,13 @@ func LoadSettingsForInstall() {
 	loadServiceFrom(CfgProvider)
 	loadMailerFrom(CfgProvider)
 }
+
+var configuredPaths = make(map[string]string)
+
+func checkOverlappedPath(name, path string) {
+	// TODO: some paths shouldn't overlap (storage.xxx.path), while some could (data path is the base path for storage path)
+	if targetName, ok := configuredPaths[path]; ok && targetName != name {
+		logStartupProblem(1, log.ERROR, "Configured path %q is used by %q and %q at the same time. The paths must be unique to prevent data loss.", path, targetName, name)
+	}
+	configuredPaths[path] = name
+}
diff --git a/modules/setting/storage.go b/modules/setting/storage.go
index f937c7cff3..aeb61ac513 100644
--- a/modules/setting/storage.go
+++ b/modules/setting/storage.go
@@ -240,6 +240,8 @@ func getStorageForLocal(targetSec, overrideSec ConfigSection, tp targetSecType,
 		}
 	}
 
+	checkOverlappedPath("[storage."+name+"].PATH", storage.Path)
+
 	return &storage, nil
 }
 
diff --git a/modules/structs/issue.go b/modules/structs/issue.go
index 34eae69329..16242d18ad 100644
--- a/modules/structs/issue.go
+++ b/modules/structs/issue.go
@@ -6,6 +6,7 @@ package structs
 import (
 	"fmt"
 	"path"
+	"slices"
 	"strings"
 	"time"
 
@@ -141,12 +142,37 @@ const (
 // IssueFormField represents a form field
 // swagger:model
 type IssueFormField struct {
-	Type        IssueFormFieldType `json:"type" yaml:"type"`
-	ID          string             `json:"id" yaml:"id"`
-	Attributes  map[string]any     `json:"attributes" yaml:"attributes"`
-	Validations map[string]any     `json:"validations" yaml:"validations"`
+	Type        IssueFormFieldType      `json:"type" yaml:"type"`
+	ID          string                  `json:"id" yaml:"id"`
+	Attributes  map[string]any          `json:"attributes" yaml:"attributes"`
+	Validations map[string]any          `json:"validations" yaml:"validations"`
+	Visible     []IssueFormFieldVisible `json:"visible,omitempty"`
 }
 
+func (iff IssueFormField) VisibleOnForm() bool {
+	if len(iff.Visible) == 0 {
+		return true
+	}
+	return slices.Contains(iff.Visible, IssueFormFieldVisibleForm)
+}
+
+func (iff IssueFormField) VisibleInContent() bool {
+	if len(iff.Visible) == 0 {
+		// we have our markdown exception
+		return iff.Type != IssueFormFieldTypeMarkdown
+	}
+	return slices.Contains(iff.Visible, IssueFormFieldVisibleContent)
+}
+
+// IssueFormFieldVisible defines issue form field visible
+// swagger:model
+type IssueFormFieldVisible string
+
+const (
+	IssueFormFieldVisibleForm    IssueFormFieldVisible = "form"
+	IssueFormFieldVisibleContent IssueFormFieldVisible = "content"
+)
+
 // IssueTemplate represents an issue template for a repository
 // swagger:model
 type IssueTemplate struct {
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index 51e175fba8..bc8eb0b756 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -90,6 +90,7 @@ type Repository struct {
 	ExternalWiki                  *ExternalWiki    `json:"external_wiki,omitempty"`
 	HasPullRequests               bool             `json:"has_pull_requests"`
 	HasProjects                   bool             `json:"has_projects"`
+	ProjectsMode                  string           `json:"projects_mode"`
 	HasReleases                   bool             `json:"has_releases"`
 	HasPackages                   bool             `json:"has_packages"`
 	HasActions                    bool             `json:"has_actions"`
@@ -98,6 +99,7 @@ type Repository struct {
 	AllowRebase                   bool             `json:"allow_rebase"`
 	AllowRebaseMerge              bool             `json:"allow_rebase_explicit"`
 	AllowSquash                   bool             `json:"allow_squash_merge"`
+	AllowFastForwardOnly          bool             `json:"allow_fast_forward_only_merge"`
 	AllowRebaseUpdate             bool             `json:"allow_rebase_update"`
 	DefaultDeleteBranchAfterMerge bool             `json:"default_delete_branch_after_merge"`
 	DefaultMergeStyle             string           `json:"default_merge_style"`
@@ -179,6 +181,8 @@ type EditRepoOption struct {
 	HasPullRequests *bool `json:"has_pull_requests,omitempty"`
 	// either `true` to enable project unit, or `false` to disable them.
 	HasProjects *bool `json:"has_projects,omitempty"`
+	// `repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.
+	ProjectsMode *string `json:"projects_mode,omitempty" binding:"In(repo,owner,all)"`
 	// either `true` to enable releases unit, or `false` to disable them.
 	HasReleases *bool `json:"has_releases,omitempty"`
 	// either `true` to enable packages unit, or `false` to disable them.
@@ -195,6 +199,8 @@ type EditRepoOption struct {
 	AllowRebaseMerge *bool `json:"allow_rebase_explicit,omitempty"`
 	// either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging.
 	AllowSquash *bool `json:"allow_squash_merge,omitempty"`
+	// either `true` to allow fast-forward-only merging pull requests, or `false` to prevent fast-forward-only merging.
+	AllowFastForwardOnly *bool `json:"allow_fast_forward_only_merge,omitempty"`
 	// either `true` to allow mark pr as merged manually, or `false` to prevent it.
 	AllowManualMerge *bool `json:"allow_manual_merge,omitempty"`
 	// either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur.
@@ -203,7 +209,7 @@ type EditRepoOption struct {
 	AllowRebaseUpdate *bool `json:"allow_rebase_update,omitempty"`
 	// set to `true` to delete pr branch after merge by default
 	DefaultDeleteBranchAfterMerge *bool `json:"default_delete_branch_after_merge,omitempty"`
-	// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", or "squash".
+	// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", "squash", or "fast-forward-only".
 	DefaultMergeStyle *string `json:"default_merge_style,omitempty"`
 	// set to `true` to allow edits from maintainers by default
 	DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`
diff --git a/modules/structs/user.go b/modules/structs/user.go
index 0df67894b0..21ecc1479e 100644
--- a/modules/structs/user.go
+++ b/modules/structs/user.go
@@ -1,4 +1,5 @@
 // Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2023 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
 package structs
@@ -108,3 +109,26 @@ type UpdateUserAvatarOption struct {
 	// image must be base64 encoded
 	Image string `json:"image" binding:"Required"`
 }
+
+// Badge represents a user badge
+// swagger:model
+type Badge struct {
+	ID          int64  `json:"id"`
+	Slug        string `json:"slug"`
+	Description string `json:"description"`
+	ImageURL    string `json:"image_url"`
+}
+
+// UserBadge represents a user badge
+// swagger:model
+type UserBadge struct {
+	ID      int64 `json:"id"`
+	BadgeID int64 `json:"badge_id"`
+	UserID  int64 `json:"user_id"`
+}
+
+// UserBadgeOption options for link between users and badges
+type UserBadgeOption struct {
+	// example: ["badge1","badge2"]
+	BadgeSlugs []string `json:"badge_slugs" binding:"Required"`
+}
diff --git a/modules/structs/variable.go b/modules/structs/variable.go
new file mode 100644
index 0000000000..cc846cf0ec
--- /dev/null
+++ b/modules/structs/variable.go
@@ -0,0 +1,37 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package structs
+
+// CreateVariableOption the option when creating variable
+// swagger:model
+type CreateVariableOption struct {
+	// Value of the variable to create
+	//
+	// required: true
+	Value string `json:"value" binding:"Required"`
+}
+
+// UpdateVariableOption the option when updating variable
+// swagger:model
+type UpdateVariableOption struct {
+	// New name for the variable. If the field is empty, the variable name won't be updated.
+	Name string `json:"name"`
+	// Value of the variable to update
+	//
+	// required: true
+	Value string `json:"value" binding:"Required"`
+}
+
+// ActionVariable return value of the query API
+// swagger:model
+type ActionVariable struct {
+	// the owner to which the variable belongs
+	OwnerID int64 `json:"owner_id"`
+	// the repository to which the variable belongs
+	RepoID int64 `json:"repo_id"`
+	// the name of the variable
+	Name string `json:"name"`
+	// the value of the variable
+	Data string `json:"data"`
+}
diff --git a/modules/svg/svg.go b/modules/svg/svg.go
index 016e1dc08b..8132978cac 100644
--- a/modules/svg/svg.go
+++ b/modules/svg/svg.go
@@ -41,6 +41,21 @@ func Init() error {
 	return nil
 }
 
+func MockIcon(icon string) func() {
+	if svgIcons == nil {
+		svgIcons = make(map[string]string)
+	}
+	orig, exist := svgIcons[icon]
+	svgIcons[icon] = fmt.Sprintf(`<svg class="svg %s" width="%d" height="%d"></svg>`, icon, defaultSize, defaultSize)
+	return func() {
+		if exist {
+			svgIcons[icon] = orig
+		} else {
+			delete(svgIcons, icon)
+		}
+	}
+}
+
 // RenderHTML renders icons - arguments icon name (string), size (int), class (string)
 func RenderHTML(icon string, others ...any) template.HTML {
 	size, class := gitea_html.ParseSizeAndClass(defaultSize, "", others...)
@@ -55,5 +70,6 @@ func RenderHTML(icon string, others ...any) template.HTML {
 		}
 		return template.HTML(svgStr)
 	}
-	return ""
+	// during test (or something wrong happens), there is no SVG loaded, so use a dummy span to tell that the icon is missing
+	return template.HTML(fmt.Sprintf("<span>%s(%d/%s)</span>", template.HTMLEscapeString(icon), size, template.HTMLEscapeString(class)))
 }
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 96cdd9ca46..5d2fa79bc5 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -9,12 +9,12 @@ import (
 	"html"
 	"html/template"
 	"net/url"
+	"slices"
 	"strings"
 	"time"
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/emoji"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/svg"
@@ -33,15 +33,16 @@ func NewFuncMap() template.FuncMap {
 
 		// -----------------------------------------------------------------
 		// html/template related functions
-		"dict":        dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
-		"Eval":        Eval,
-		"Safe":        Safe,
-		"Escape":      html.EscapeString,
-		"QueryEscape": url.QueryEscape,
-		"JSEscape":    template.JSEscapeString,
-		"Str2html":    Str2html, // TODO: rename it to SanitizeHTML
-		"URLJoin":     util.URLJoin,
-		"DotEscape":   DotEscape,
+		"dict":         dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
+		"Eval":         Eval,
+		"SafeHTML":     SafeHTML,
+		"HTMLFormat":   HTMLFormat,
+		"HTMLEscape":   HTMLEscape,
+		"QueryEscape":  QueryEscape,
+		"JSEscape":     JSEscapeSafe,
+		"SanitizeHTML": SanitizeHTML,
+		"URLJoin":      util.URLJoin,
+		"DotEscape":    DotEscape,
 
 		"PathEscape":         url.PathEscape,
 		"PathEscapeSegments": util.PathEscapeSegments,
@@ -52,13 +53,13 @@ func NewFuncMap() template.FuncMap {
 		"JsonUtils":   NewJsonUtils,
 
 		// -----------------------------------------------------------------
-		// svg / avatar / icon
+		// svg / avatar / icon / color
 		"svg":           svg.RenderHTML,
 		"EntryIcon":     base.EntryIcon,
 		"MigrationIcon": MigrationIcon,
 		"ActionIcon":    ActionIcon,
-
-		"SortArrow": SortArrow,
+		"SortArrow":     SortArrow,
+		"ContrastColor": util.ContrastColor,
 
 		// -----------------------------------------------------------------
 		// time / number / format
@@ -105,6 +106,9 @@ func NewFuncMap() template.FuncMap {
 		"ShowFooterTemplateLoadTime": func() bool {
 			return setting.Other.ShowFooterTemplateLoadTime
 		},
+		"ShowFooterPoweredBy": func() bool {
+			return setting.Other.ShowFooterPoweredBy
+		},
 		"AllowedReactions": func() []string {
 			return setting.UI.Reactions
 		},
@@ -159,7 +163,6 @@ func NewFuncMap() template.FuncMap {
 		"RenderCodeBlock":  RenderCodeBlock,
 		"RenderIssueTitle": RenderIssueTitle,
 		"RenderEmoji":      RenderEmoji,
-		"RenderEmojiPlain": emoji.ReplaceAliases,
 		"ReactionToEmoji":  ReactionToEmoji,
 
 		"RenderMarkdownToHtml": RenderMarkdownToHtml,
@@ -179,14 +182,55 @@ func NewFuncMap() template.FuncMap {
 	}
 }
 
-// Safe render raw as HTML
-func Safe(raw string) template.HTML {
-	return template.HTML(raw)
+func HTMLFormat(s string, rawArgs ...any) template.HTML {
+	args := slices.Clone(rawArgs)
+	for i, v := range args {
+		switch v := v.(type) {
+		case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
+			// for most basic types (including template.HTML which is safe), just do nothing and use it
+		case string:
+			args[i] = template.HTMLEscapeString(v)
+		case fmt.Stringer:
+			args[i] = template.HTMLEscapeString(v.String())
+		default:
+			args[i] = template.HTMLEscapeString(fmt.Sprint(v))
+		}
+	}
+	return template.HTML(fmt.Sprintf(s, args...))
 }
 
-// Str2html render Markdown text to HTML
-func Str2html(raw string) template.HTML {
-	return template.HTML(markup.Sanitize(raw))
+// SafeHTML render raw as HTML
+func SafeHTML(s any) template.HTML {
+	switch v := s.(type) {
+	case string:
+		return template.HTML(v)
+	case template.HTML:
+		return v
+	}
+	panic(fmt.Sprintf("unexpected type %T", s))
+}
+
+// SanitizeHTML sanitizes the input by pre-defined markdown rules
+func SanitizeHTML(s string) template.HTML {
+	return template.HTML(markup.Sanitize(s))
+}
+
+func HTMLEscape(s any) template.HTML {
+	switch v := s.(type) {
+	case string:
+		return template.HTML(html.EscapeString(v))
+	case template.HTML:
+		return v
+	}
+	panic(fmt.Sprintf("unexpected type %T", s))
+}
+
+func JSEscapeSafe(s string) template.HTML {
+	return template.HTML(template.JSEscapeString(s))
+}
+
+func QueryEscape(s string) template.URL {
+	return template.URL(url.QueryEscape(s))
 }
 
 // DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index ec83e9ac33..64f29d033e 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -4,6 +4,7 @@
 package templates
 
 import (
+	"html/template"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -52,3 +53,15 @@ func TestSubjectBodySeparator(t *testing.T) {
 		"",
 		"Insuficient\n--\nSeparators")
 }
+
+func TestJSEscapeSafe(t *testing.T) {
+	assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, JSEscapeSafe(`&<>'"`))
+}
+
+func TestHTMLFormat(t *testing.T) {
+	assert.Equal(t, template.HTML("<a>&lt; < 1</a>"), HTMLFormat("<a>%s %s %d</a>", "<", template.HTML("<"), 1))
+}
+
+func TestSanitizeHTML(t *testing.T) {
+	assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
+}
diff --git a/modules/templates/mailer.go b/modules/templates/mailer.go
index 54d857a8f6..f1832cba0e 100644
--- a/modules/templates/mailer.go
+++ b/modules/templates/mailer.go
@@ -5,6 +5,7 @@ package templates
 
 import (
 	"context"
+	"fmt"
 	"html/template"
 	"regexp"
 	"strings"
@@ -33,7 +34,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap {
 	}
 }
 
-func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
+func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) error {
 	// Split template into subject and body
 	var subjectContent []byte
 	bodyContent := content
@@ -42,14 +43,13 @@ func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template,
 		subjectContent = content[0:loc[0]]
 		bodyContent = content[loc[1]:]
 	}
-	if _, err := stpl.New(name).
-		Parse(string(subjectContent)); err != nil {
-		log.Warn("Failed to parse template [%s/subject]: %v", name, err)
+	if _, err := stpl.New(name).Parse(string(subjectContent)); err != nil {
+		return fmt.Errorf("failed to parse template [%s/subject]: %w", name, err)
 	}
-	if _, err := btpl.New(name).
-		Parse(string(bodyContent)); err != nil {
-		log.Warn("Failed to parse template [%s/body]: %v", name, err)
+	if _, err := btpl.New(name).Parse(string(bodyContent)); err != nil {
+		return fmt.Errorf("failed to parse template [%s/body]: %w", name, err)
 	}
+	return nil
 }
 
 // Mailer provides the templates required for sending notification mails.
@@ -81,7 +81,13 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
 			if firstRun {
 				log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName)
 			}
-			buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content)
+			if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil {
+				if firstRun {
+					log.Fatal("Failed to parse mail template, err: %v", err)
+				} else {
+					log.Error("Failed to parse mail template, err: %v", err)
+				}
+			}
 		}
 	}
 
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
index 1d9635410b..0b53965f25 100644
--- a/modules/templates/util_render.go
+++ b/modules/templates/util_render.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/modules/util"
 )
 
@@ -40,7 +41,7 @@ func RenderCommitMessage(ctx context.Context, msg string, metas map[string]strin
 	if len(msgLines) == 0 {
 		return template.HTML("")
 	}
-	return template.HTML(msgLines[0])
+	return RenderCodeBlock(template.HTML(msgLines[0]))
 }
 
 // RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
@@ -67,7 +68,7 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string,
 		log.Error("RenderCommitMessageSubject: %v", err)
 		return template.HTML("")
 	}
-	return template.HTML(renderedMessage)
+	return RenderCodeBlock(template.HTML(renderedMessage))
 }
 
 // RenderCommitBody extracts the body of a commit message without its title.
@@ -118,22 +119,25 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
 }
 
 // RenderLabel renders a label
-func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
-	labelScope := label.ExclusiveScope()
-
-	textColor := "#111"
-	r, g, b := util.HexToRBGColor(label.Color)
-	// Determine if label text should be light or dark to be readable on background color
-	if util.UseLightTextOnBackground(r, g, b) {
-		textColor = "#eee"
-	}
+// locale is needed due to an import cycle with our context providing the `Tr` function
+func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
+	var (
+		archivedCSSClass string
+		textColor        = util.ContrastColor(label.Color)
+		labelScope       = label.ExclusiveScope()
+	)
 
 	description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
 
+	if label.IsArchived() {
+		archivedCSSClass = "archived-label"
+		description = fmt.Sprintf("(%s) %s", locale.TrString("archived"), description)
+	}
+
 	if labelScope == "" {
 		// Regular label
-		s := fmt.Sprintf("<div class='ui label' style='color: %s !important; background-color: %s !important' data-tooltip-content title='%s'>%s</div>",
-			textColor, label.Color, description, RenderEmoji(ctx, label.Name))
+		s := fmt.Sprintf("<div class='ui label %s' style='color: %s !important; background-color: %s !important;' data-tooltip-content title='%s'>%s</div>",
+			archivedCSSClass, textColor, label.Color, description, RenderEmoji(ctx, label.Name))
 		return template.HTML(s)
 	}
 
@@ -143,7 +147,7 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
 
 	// Make scope and item background colors slightly darker and lighter respectively.
 	// More contrast needed with higher luminance, empirically tweaked.
-	luminance := util.GetLuminance(r, g, b)
+	luminance := util.GetRelativeLuminance(label.Color)
 	contrast := 0.01 + luminance*0.03
 	// Ensure we add the same amount of contrast also near 0 and 1.
 	darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
@@ -152,6 +156,7 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
 	darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
 	lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
 
+	r, g, b := util.HexToRBGColor(label.Color)
 	scopeBytes := []byte{
 		uint8(math.Min(math.Round(r*darkenFactor), 255)),
 		uint8(math.Min(math.Round(g*darkenFactor), 255)),
@@ -166,11 +171,11 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
 	itemColor := "#" + hex.EncodeToString(itemBytes)
 	scopeColor := "#" + hex.EncodeToString(scopeBytes)
 
-	s := fmt.Sprintf("<span class='ui label scope-parent' data-tooltip-content title='%s'>"+
+	s := fmt.Sprintf("<span class='ui label %s scope-parent' data-tooltip-content title='%s'>"+
 		"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
 		"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important'>%s</div>"+
 		"</span>",
-		description,
+		archivedCSSClass, description,
 		textColor, scopeColor, scopeText,
 		textColor, itemColor, itemText)
 	return template.HTML(s)
@@ -208,10 +213,10 @@ func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //n
 	if err != nil {
 		log.Error("RenderString: %v", err)
 	}
-	return template.HTML(output)
+	return output
 }
 
-func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
+func RenderLabels(ctx context.Context, locale translation.Locale, labels []*issues_model.Label, repoLink string) template.HTML {
 	htmlCode := `<span class="labels-list">`
 	for _, label := range labels {
 		// Protect against nil value in labels - shouldn't happen but would cause a panic if so
@@ -219,7 +224,7 @@ func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink st
 			continue
 		}
 		htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
-			repoLink, label.ID, RenderLabel(ctx, label))
+			repoLink, label.ID, RenderLabel(ctx, locale, label))
 	}
 	htmlCode += "</span>"
 	return template.HTML(htmlCode)
diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go
index 8648967d38..15aee8912d 100644
--- a/modules/templates/util_render_test.go
+++ b/modules/templates/util_render_test.go
@@ -117,21 +117,21 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a582
 com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 <span class="emoji" aria-label="thumbs up">👍</span>
 <a href="mailto:mail@domain.com" class="mailto">mail@domain.com</a>
-<a href="http://localhost:3000/mention-user" class="mention">@mention-user</a> test
-<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a>
+<a href="/mention-user" class="mention">@mention-user</a> test
+<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
   space`
 
 	assert.EqualValues(t, expected, RenderCommitBody(context.Background(), testInput, testMetas))
 }
 
 func TestRenderCommitMessage(t *testing.T) {
-	expected := `space <a href="http://localhost:3000/mention-user" class="mention">@mention-user</a>  `
+	expected := `space <a href="/mention-user" class="mention">@mention-user</a>  `
 
 	assert.EqualValues(t, expected, RenderCommitMessage(context.Background(), testInput, testMetas))
 }
 
 func TestRenderCommitMessageLinkSubject(t *testing.T) {
-	expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="http://localhost:3000/mention-user" class="mention">@mention-user</a>`
+	expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="/mention-user" class="mention">@mention-user</a>`
 
 	assert.EqualValues(t, expected, RenderCommitMessageLinkSubject(context.Background(), testInput, "https://example.com/link", testMetas))
 }
@@ -155,14 +155,14 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 <span class="emoji" aria-label="thumbs up">👍</span>
 mail@domain.com
 @mention-user test
-<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a>
+<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
   space  
 `
 	assert.EqualValues(t, expected, RenderIssueTitle(context.Background(), testInput, testMetas))
 }
 
 func TestRenderMarkdownToHtml(t *testing.T) {
-	expected := `<p>space <a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a><br/>
+	expected := `<p>space <a href="/mention-user" rel="nofollow">@mention-user</a><br/>
 /just/a/path.bin
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
 <a href="/file.bin" rel="nofollow">local link</a>
@@ -179,7 +179,7 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a582
 com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 <span class="emoji" aria-label="thumbs up">👍</span>
 <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
-<a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a> test
+<a href="/mention-user" rel="nofollow">@mention-user</a> test
 #123
 space</p>
 `
diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go
index 2771b1e223..479b755da1 100644
--- a/modules/templates/util_string.go
+++ b/modules/templates/util_string.go
@@ -4,6 +4,8 @@
 package templates
 
 import (
+	"fmt"
+	"html/template"
 	"strings"
 
 	"code.gitea.io/gitea/modules/base"
@@ -17,6 +19,19 @@ func NewStringUtils() *StringUtils {
 	return &stringUtils
 }
 
+func (su *StringUtils) ToString(v any) string {
+	switch v := v.(type) {
+	case string:
+		return v
+	case template.HTML:
+		return string(v)
+	case fmt.Stringer:
+		return v.String()
+	default:
+		return fmt.Sprint(v)
+	}
+}
+
 func (su *StringUtils) HasPrefix(s, prefix string) bool {
 	return strings.HasPrefix(s, prefix)
 }
diff --git a/modules/timeutil/datetime.go b/modules/timeutil/datetime.go
index 62b94f7cf4..c089173560 100644
--- a/modules/timeutil/datetime.go
+++ b/modules/timeutil/datetime.go
@@ -13,6 +13,8 @@ import (
 
 // DateTime renders an absolute time HTML element by datetime.
 func DateTime(format string, datetime any, extraAttrs ...string) template.HTML {
+	// TODO: remove the extraAttrs argument, it's not used in any call to DateTime
+
 	if p, ok := datetime.(*time.Time); ok {
 		datetime = *p
 	}
@@ -51,18 +53,16 @@ func DateTime(format string, datetime any, extraAttrs ...string) template.HTML {
 
 	attrs := make([]string, 0, 10+len(extraAttrs))
 	attrs = append(attrs, extraAttrs...)
-	attrs = append(attrs, `data-tooltip-content`, `data-tooltip-interactive="true"`)
-	attrs = append(attrs, `format="datetime"`, `weekday=""`, `year="numeric"`)
+	attrs = append(attrs, `weekday=""`, `year="numeric"`)
 
 	switch format {
-	case "short":
-		attrs = append(attrs, `month="short"`, `day="numeric"`)
-	case "long":
-		attrs = append(attrs, `month="long"`, `day="numeric"`)
-	case "full":
-		attrs = append(attrs, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`)
+	case "short", "long": // date only
+		attrs = append(attrs, `month="`+format+`"`, `day="numeric"`)
+		return template.HTML(fmt.Sprintf(`<absolute-date %s date="%s">%s</absolute-date>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
+	case "full": // full date including time
+		attrs = append(attrs, `format="datetime"`, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`, `data-tooltip-content`, `data-tooltip-interactive="true"`)
+		return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
 	default:
 		panic(fmt.Sprintf("Unsupported format %s", format))
 	}
-	return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
 }
diff --git a/modules/timeutil/datetime_test.go b/modules/timeutil/datetime_test.go
index 26494b8475..ac2ce35ba2 100644
--- a/modules/timeutil/datetime_test.go
+++ b/modules/timeutil/datetime_test.go
@@ -18,6 +18,7 @@ func TestDateTime(t *testing.T) {
 	defer test.MockVariableValue(&setting.DefaultUILocation, testTz)()
 
 	refTimeStr := "2018-01-01T00:00:00Z"
+	refDateStr := "2018-01-01"
 	refTime, _ := time.Parse(time.RFC3339, refTimeStr)
 	refTimeStamp := TimeStamp(refTime.Unix())
 
@@ -27,17 +28,20 @@ func TestDateTime(t *testing.T) {
 	assert.EqualValues(t, "-", DateTime("short", TimeStamp(0)))
 
 	actual := DateTime("short", "invalid")
-	assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="invalid">invalid</relative-time>`, actual)
+	assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="invalid">invalid</absolute-date>`, actual)
 
 	actual = DateTime("short", refTimeStr)
-	assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</relative-time>`, actual)
+	assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</absolute-date>`, actual)
 
 	actual = DateTime("short", refTime)
-	assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="2018-01-01T00:00:00Z">2018-01-01</relative-time>`, actual)
+	assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01</absolute-date>`, actual)
+
+	actual = DateTime("short", refDateStr)
+	assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01">2018-01-01</absolute-date>`, actual)
 
 	actual = DateTime("short", refTimeStamp)
-	assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="2017-12-31T19:00:00-05:00">2017-12-31</relative-time>`, actual)
+	assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2017-12-31T19:00:00-05:00">2017-12-31</absolute-date>`, actual)
 
 	actual = DateTime("full", refTimeStamp)
-	assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, actual)
+	assert.EqualValues(t, `<relative-time weekday="" year="numeric" format="datetime" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" data-tooltip-content data-tooltip-interactive="true" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, actual)
 }
diff --git a/modules/timeutil/since.go b/modules/timeutil/since.go
index 1cb3c4f288..dba42c793a 100644
--- a/modules/timeutil/since.go
+++ b/modules/timeutil/since.go
@@ -28,54 +28,54 @@ func computeTimeDiffFloor(diff int64, lang translation.Locale) (int64, string) {
 	switch {
 	case diff <= 0:
 		diff = 0
-		diffStr = lang.Tr("tool.now")
+		diffStr = lang.TrString("tool.now")
 	case diff < 2:
 		diff = 0
-		diffStr = lang.Tr("tool.1s")
+		diffStr = lang.TrString("tool.1s")
 	case diff < 1*Minute:
-		diffStr = lang.Tr("tool.seconds", diff)
+		diffStr = lang.TrString("tool.seconds", diff)
 		diff = 0
 
 	case diff < 2*Minute:
 		diff -= 1 * Minute
-		diffStr = lang.Tr("tool.1m")
+		diffStr = lang.TrString("tool.1m")
 	case diff < 1*Hour:
-		diffStr = lang.Tr("tool.minutes", diff/Minute)
+		diffStr = lang.TrString("tool.minutes", diff/Minute)
 		diff -= diff / Minute * Minute
 
 	case diff < 2*Hour:
 		diff -= 1 * Hour
-		diffStr = lang.Tr("tool.1h")
+		diffStr = lang.TrString("tool.1h")
 	case diff < 1*Day:
-		diffStr = lang.Tr("tool.hours", diff/Hour)
+		diffStr = lang.TrString("tool.hours", diff/Hour)
 		diff -= diff / Hour * Hour
 
 	case diff < 2*Day:
 		diff -= 1 * Day
-		diffStr = lang.Tr("tool.1d")
+		diffStr = lang.TrString("tool.1d")
 	case diff < 1*Week:
-		diffStr = lang.Tr("tool.days", diff/Day)
+		diffStr = lang.TrString("tool.days", diff/Day)
 		diff -= diff / Day * Day
 
 	case diff < 2*Week:
 		diff -= 1 * Week
-		diffStr = lang.Tr("tool.1w")
+		diffStr = lang.TrString("tool.1w")
 	case diff < 1*Month:
-		diffStr = lang.Tr("tool.weeks", diff/Week)
+		diffStr = lang.TrString("tool.weeks", diff/Week)
 		diff -= diff / Week * Week
 
 	case diff < 2*Month:
 		diff -= 1 * Month
-		diffStr = lang.Tr("tool.1mon")
+		diffStr = lang.TrString("tool.1mon")
 	case diff < 1*Year:
-		diffStr = lang.Tr("tool.months", diff/Month)
+		diffStr = lang.TrString("tool.months", diff/Month)
 		diff -= diff / Month * Month
 
 	case diff < 2*Year:
 		diff -= 1 * Year
-		diffStr = lang.Tr("tool.1y")
+		diffStr = lang.TrString("tool.1y")
 	default:
-		diffStr = lang.Tr("tool.years", diff/Year)
+		diffStr = lang.TrString("tool.years", diff/Year)
 		diff -= (diff / Year) * Year
 	}
 	return diff, diffStr
@@ -97,10 +97,10 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
 	diff := now.Unix() - then.Unix()
 
 	if then.After(now) {
-		return lang.Tr("tool.future")
+		return lang.TrString("tool.future")
 	}
 	if diff == 0 {
-		return lang.Tr("tool.now")
+		return lang.TrString("tool.now")
 	}
 
 	var timeStr, diffStr string
@@ -115,7 +115,7 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
 	return strings.TrimPrefix(timeStr, ", ")
 }
 
-func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML {
+func timeSinceUnix(then, now time.Time, _ translation.Locale) template.HTML {
 	friendlyText := then.Format("2006-01-02 15:04:05 -07:00")
 
 	// document: https://github.com/github/relative-time-element
@@ -126,7 +126,7 @@ func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML {
 	}
 
 	// declare data-tooltip-content attribute to switch from "title" tooltip to "tippy" tooltip
-	htm := fmt.Sprintf(`<relative-time class="time-since" prefix="" %s datetime="%s" data-tooltip-content data-tooltip-interactive="true">%s</relative-time>`,
+	htm := fmt.Sprintf(`<relative-time prefix="" %s datetime="%s" data-tooltip-content data-tooltip-interactive="true">%s</relative-time>`,
 		attrs, then.Format(time.RFC3339), friendlyText)
 	return template.HTML(htm)
 }
@@ -134,7 +134,7 @@ func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML {
 // TimeSince renders relative time HTML given a time.Time
 func TimeSince(then time.Time, lang translation.Locale) template.HTML {
 	if setting.UI.PreferredTimestampTense == "absolute" {
-		return DateTime("full", then, `class="time-since"`)
+		return DateTime("full", then)
 	}
 	return timeSinceUnix(then, time.Now(), lang)
 }
diff --git a/modules/timeutil/timestamp.go b/modules/timeutil/timestamp.go
index 27a80b6682..e77652b24f 100644
--- a/modules/timeutil/timestamp.go
+++ b/modules/timeutil/timestamp.go
@@ -21,8 +21,9 @@ var (
 )
 
 // MockSet sets the time to a mocked time.Time
-func MockSet(now time.Time) {
+func MockSet(now time.Time) func() {
 	mockNow = now
+	return MockUnset
 }
 
 // MockUnset will unset the mocked time.Time
diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go
index 42475545b3..1555cd961e 100644
--- a/modules/translation/i18n/i18n.go
+++ b/modules/translation/i18n/i18n.go
@@ -4,26 +4,25 @@
 package i18n
 
 import (
+	"html/template"
 	"io"
 )
 
 var DefaultLocales = NewLocaleStore()
 
 type Locale interface {
-	// Tr translates a given key and arguments for a language
-	Tr(trKey string, trArgs ...any) string
-	// Has reports if a locale has a translation for a given key
-	Has(trKey string) bool
+	// TrString translates a given key and arguments for a language
+	TrString(trKey string, trArgs ...any) string
+	// TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML
+	TrHTML(trKey string, trArgs ...any) template.HTML
+	// HasKey reports if a locale has a translation for a given key
+	HasKey(trKey string) bool
 }
 
 // LocaleStore provides the functions common to all locale stores
 type LocaleStore interface {
 	io.Closer
 
-	// Tr translates a given key and arguments for a language
-	Tr(lang, trKey string, trArgs ...any) string
-	// Has reports if a locale has a translation for a given key
-	Has(lang, trKey string) bool
 	// SetDefaultLang sets the default language to fall back to
 	SetDefaultLang(lang string)
 	// ListLangNameDesc provides paired slices of language names to descriptors
@@ -45,7 +44,7 @@ func ResetDefaultLocales() {
 	DefaultLocales = NewLocaleStore()
 }
 
-// GetLocales returns the locale from the default locales
+// GetLocale returns the locale from the default locales
 func GetLocale(lang string) (Locale, bool) {
 	return DefaultLocales.Locale(lang)
 }
diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go
index 1d1be43318..b364992dfe 100644
--- a/modules/translation/i18n/i18n_test.go
+++ b/modules/translation/i18n/i18n_test.go
@@ -4,6 +4,7 @@
 package i18n
 
 import (
+	"html/template"
 	"strings"
 	"testing"
 
@@ -17,7 +18,7 @@ fmt = %[1]s %[2]s
 
 [section]
 sub = Sub String
-mixed = test value; <span style="color: red\; background: none;">more text</span>
+mixed = test value; <span style="color: red\; background: none;">%s</span>
 `)
 
 	testData2 := []byte(`
@@ -32,29 +33,33 @@ sub = Changed Sub String
 	assert.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil))
 	ls.SetDefaultLang("lang1")
 
-	result := ls.Tr("lang1", "fmt", "a", "b")
+	lang1, _ := ls.Locale("lang1")
+	lang2, _ := ls.Locale("lang2")
+
+	result := lang1.TrString("fmt", "a", "b")
 	assert.Equal(t, "a b", result)
 
-	result = ls.Tr("lang2", "fmt", "a", "b")
+	result = lang2.TrString("fmt", "a", "b")
 	assert.Equal(t, "b a", result)
 
-	result = ls.Tr("lang1", "section.sub")
+	result = lang1.TrString("section.sub")
 	assert.Equal(t, "Sub String", result)
 
-	result = ls.Tr("lang2", "section.sub")
+	result = lang2.TrString("section.sub")
 	assert.Equal(t, "Changed Sub String", result)
 
-	result = ls.Tr("", ".dot.name")
+	langNone, _ := ls.Locale("none")
+	result = langNone.TrString(".dot.name")
 	assert.Equal(t, "Dot Name", result)
 
-	result = ls.Tr("lang2", "section.mixed")
-	assert.Equal(t, `test value; <span style="color: red; background: none;">more text</span>`, result)
+	result2 := lang2.TrHTML("section.mixed", "a&b")
+	assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&amp;b</span>`, result2)
 
 	langs, descs := ls.ListLangNameDesc()
 	assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs)
 	assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs)
 
-	found := ls.Has("lang1", "no-such")
+	found := lang1.HasKey("no-such")
 	assert.False(t, found)
 	assert.NoError(t, ls.Close())
 }
@@ -72,9 +77,75 @@ c=22
 
 	ls := NewLocaleStore()
 	assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2))
-	assert.Equal(t, "11", ls.Tr("lang1", "a"))
-	assert.Equal(t, "21", ls.Tr("lang1", "b"))
-	assert.Equal(t, "22", ls.Tr("lang1", "c"))
+	lang1, _ := ls.Locale("lang1")
+	assert.Equal(t, "11", lang1.TrString("a"))
+	assert.Equal(t, "21", lang1.TrString("b"))
+	assert.Equal(t, "22", lang1.TrString("c"))
+}
+
+type stringerPointerReceiver struct {
+	s string
+}
+
+func (s *stringerPointerReceiver) String() string {
+	return s.s
+}
+
+type stringerStructReceiver struct {
+	s string
+}
+
+func (s stringerStructReceiver) String() string {
+	return s.s
+}
+
+type errorStructReceiver struct {
+	s string
+}
+
+func (e errorStructReceiver) Error() string {
+	return e.s
+}
+
+type errorPointerReceiver struct {
+	s string
+}
+
+func (e *errorPointerReceiver) Error() string {
+	return e.s
+}
+
+func TestLocaleWithTemplate(t *testing.T) {
+	ls := NewLocaleStore()
+	assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=<a>%s</a>`), nil))
+	lang1, _ := ls.Locale("lang1")
+
+	tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML})
+	tmpl = template.Must(tmpl.Parse(`{{tr "key" .var}}`))
+
+	cases := []struct {
+		in   any
+		want string
+	}{
+		{"<str>", "<a>&lt;str&gt;</a>"},
+		{[]byte("<bytes>"), "<a>[60 98 121 116 101 115 62]</a>"},
+		{template.HTML("<html>"), "<a><html></a>"},
+		{stringerPointerReceiver{"<stringerPointerReceiver>"}, "<a>{&lt;stringerPointerReceiver&gt;}</a>"},
+		{&stringerPointerReceiver{"<stringerPointerReceiver ptr>"}, "<a>&lt;stringerPointerReceiver ptr&gt;</a>"},
+		{stringerStructReceiver{"<stringerStructReceiver>"}, "<a>&lt;stringerStructReceiver&gt;</a>"},
+		{&stringerStructReceiver{"<stringerStructReceiver ptr>"}, "<a>&lt;stringerStructReceiver ptr&gt;</a>"},
+		{errorStructReceiver{"<errorStructReceiver>"}, "<a>&lt;errorStructReceiver&gt;</a>"},
+		{&errorStructReceiver{"<errorStructReceiver ptr>"}, "<a>&lt;errorStructReceiver ptr&gt;</a>"},
+		{errorPointerReceiver{"<errorPointerReceiver>"}, "<a>{&lt;errorPointerReceiver&gt;}</a>"},
+		{&errorPointerReceiver{"<errorPointerReceiver ptr>"}, "<a>&lt;errorPointerReceiver ptr&gt;</a>"},
+	}
+
+	buf := &strings.Builder{}
+	for _, c := range cases {
+		buf.Reset()
+		assert.NoError(t, tmpl.Execute(buf, map[string]any{"var": c.in}))
+		assert.Equal(t, c.want, buf.String())
+	}
 }
 
 func TestLocaleStoreQuirks(t *testing.T) {
@@ -110,8 +181,9 @@ func TestLocaleStoreQuirks(t *testing.T) {
 	for _, testData := range testDataList {
 		ls := NewLocaleStore()
 		err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil)
+		lang1, _ := ls.Locale("lang1")
 		assert.NoError(t, err, testData.hint)
-		assert.Equal(t, testData.out, ls.Tr("lang1", "a"), testData.hint)
+		assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint)
 		assert.NoError(t, ls.Close())
 	}
 
diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go
index f5a951a79f..b422996984 100644
--- a/modules/translation/i18n/localestore.go
+++ b/modules/translation/i18n/localestore.go
@@ -5,6 +5,8 @@ package i18n
 
 import (
 	"fmt"
+	"html/template"
+	"slices"
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
@@ -18,6 +20,8 @@ type locale struct {
 	idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
 }
 
+var _ Locale = (*locale)(nil)
+
 type localeStore struct {
 	// After initializing has finished, these fields are read-only.
 	langNames []string
@@ -85,20 +89,6 @@ func (store *localeStore) SetDefaultLang(lang string) {
 	store.defaultLang = lang
 }
 
-// Tr translates content to target language. fall back to default language.
-func (store *localeStore) Tr(lang, trKey string, trArgs ...any) string {
-	l, _ := store.Locale(lang)
-
-	return l.Tr(trKey, trArgs...)
-}
-
-// Has returns whether the given language has a translation for the provided key
-func (store *localeStore) Has(lang, trKey string) bool {
-	l, _ := store.Locale(lang)
-
-	return l.Has(trKey)
-}
-
 // Locale returns the locale for the lang or the default language
 func (store *localeStore) Locale(lang string) (Locale, bool) {
 	l, found := store.localeMap[lang]
@@ -113,13 +103,11 @@ func (store *localeStore) Locale(lang string) (Locale, bool) {
 	return l, found
 }
 
-// Close implements io.Closer
 func (store *localeStore) Close() error {
 	return nil
 }
 
-// Tr translates content to locale language. fall back to default language.
-func (l *locale) Tr(trKey string, trArgs ...any) string {
+func (l *locale) TrString(trKey string, trArgs ...any) string {
 	format := trKey
 
 	idx, ok := l.store.trKeyToIdxMap[trKey]
@@ -141,8 +129,25 @@ func (l *locale) Tr(trKey string, trArgs ...any) string {
 	return msg
 }
 
-// Has returns whether a key is present in this locale or not
-func (l *locale) Has(trKey string) bool {
+func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
+	args := slices.Clone(trArgs)
+	for i, v := range args {
+		switch v := v.(type) {
+		case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
+			// for most basic types (including template.HTML which is safe), just do nothing and use it
+		case string:
+			args[i] = template.HTMLEscapeString(v)
+		case fmt.Stringer:
+			args[i] = template.HTMLEscapeString(v.String())
+		default:
+			args[i] = template.HTMLEscapeString(fmt.Sprint(v))
+		}
+	}
+	return template.HTML(l.TrString(trKey, args...))
+}
+
+// HasKey returns whether a key is present in this locale or not
+func (l *locale) HasKey(trKey string) bool {
 	idx, ok := l.store.trKeyToIdxMap[trKey]
 	if !ok {
 		return false
diff --git a/modules/translation/mock.go b/modules/translation/mock.go
index 2d0cb17324..f457271ea5 100644
--- a/modules/translation/mock.go
+++ b/modules/translation/mock.go
@@ -3,10 +3,16 @@
 
 package translation
 
-import "fmt"
+import (
+	"fmt"
+	"html/template"
+	"strings"
+)
 
 // MockLocale provides a mocked locale without any translations
-type MockLocale struct{}
+type MockLocale struct {
+	Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
+}
 
 var _ Locale = (*MockLocale)(nil)
 
@@ -14,14 +20,25 @@ func (l MockLocale) Language() string {
 	return "en"
 }
 
-func (l MockLocale) Tr(s string, _ ...any) string {
-	return s
+func (l MockLocale) TrString(s string, args ...any) string {
+	return sprintAny(s, args...)
 }
 
-func (l MockLocale) TrN(_cnt any, key1, _keyN string, _args ...any) string {
-	return key1
+func (l MockLocale) Tr(s string, args ...any) template.HTML {
+	return template.HTML(sprintAny(s, args...))
+}
+
+func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
+	return template.HTML(sprintAny(key1, args...))
 }
 
 func (l MockLocale) PrettyNumber(v any) string {
 	return fmt.Sprint(v)
 }
+
+func sprintAny(s string, args ...any) string {
+	if len(args) == 0 {
+		return s
+	}
+	return s + ":" + fmt.Sprintf(strings.Repeat(",%v", len(args))[1:], args...)
+}
diff --git a/modules/translation/translation.go b/modules/translation/translation.go
index dba4de6607..36ae58a9f1 100644
--- a/modules/translation/translation.go
+++ b/modules/translation/translation.go
@@ -5,6 +5,7 @@ package translation
 
 import (
 	"context"
+	"html/template"
 	"sort"
 	"strings"
 	"sync"
@@ -27,8 +28,11 @@ var ContextKey any = &contextKey{}
 // Locale represents an interface to translation
 type Locale interface {
 	Language() string
-	Tr(string, ...any) string
-	TrN(cnt any, key1, keyN string, args ...any) string
+	TrString(string, ...any) string
+
+	Tr(key string, args ...any) template.HTML
+	TrN(cnt any, key1, keyN string, args ...any) template.HTML
+
 	PrettyNumber(v any) string
 }
 
@@ -140,10 +144,12 @@ func Match(tags ...language.Tag) language.Tag {
 // locale represents the information of localization.
 type locale struct {
 	i18n.Locale
-	Lang, LangName string // these fields are used directly in templates: .i18n.Lang
+	Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
 	msgPrinter     *message.Printer
 }
 
+var _ Locale = (*locale)(nil)
+
 // NewLocale return a locale
 func NewLocale(lang string) Locale {
 	if lock != nil {
@@ -216,8 +222,12 @@ var trNLangRules = map[string]func(int64) int{
 	},
 }
 
+func (l *locale) Tr(s string, args ...any) template.HTML {
+	return l.TrHTML(s, args...)
+}
+
 // TrN returns translated message for plural text translation
-func (l *locale) TrN(cnt any, key1, keyN string, args ...any) string {
+func (l *locale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
 	var c int64
 	if t, ok := cnt.(int); ok {
 		c = int64(t)
diff --git a/modules/util/color.go b/modules/util/color.go
index 240b045c28..9c520dce78 100644
--- a/modules/util/color.go
+++ b/modules/util/color.go
@@ -4,22 +4,10 @@ package util
 
 import (
 	"fmt"
-	"math"
 	"strconv"
 	"strings"
 )
 
-// Check similar implementation in web_src/js/utils/color.js and keep synchronization
-
-// Return R, G, B values defined in reletive luminance
-func getLuminanceRGB(channel float64) float64 {
-	sRGB := channel / 255
-	if sRGB <= 0.03928 {
-		return sRGB / 12.92
-	}
-	return math.Pow((sRGB+0.055)/1.055, 2.4)
-}
-
 // Get color as RGB values in 0..255 range from the hex color string (with or without #)
 func HexToRBGColor(colorString string) (float64, float64, float64) {
 	hexString := colorString
@@ -47,19 +35,23 @@ func HexToRBGColor(colorString string) (float64, float64, float64) {
 	return r, g, b
 }
 
-// return luminance given RGB channels
-// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
-func GetLuminance(r, g, b float64) float64 {
-	R := getLuminanceRGB(r)
-	G := getLuminanceRGB(g)
-	B := getLuminanceRGB(b)
-	luminance := 0.2126*R + 0.7152*G + 0.0722*B
-	return luminance
+// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
+// Keep this in sync with web_src/js/utils/color.js
+func GetRelativeLuminance(color string) float64 {
+	r, g, b := HexToRBGColor(color)
+	return (0.2126729*r + 0.7151522*g + 0.0721750*b) / 255
 }
 
-// Reference from: https://firsching.ch/github_labels.html
-// In the future WCAG 3 APCA may be a better solution.
-// Check if text should use light color based on RGB of background
-func UseLightTextOnBackground(r, g, b float64) bool {
-	return GetLuminance(r, g, b) < 0.453
+func UseLightText(backgroundColor string) bool {
+	return GetRelativeLuminance(backgroundColor) < 0.453
+}
+
+// Given a background color, returns a black or white foreground color that the highest
+// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
+// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
+func ContrastColor(backgroundColor string) string {
+	if UseLightText(backgroundColor) {
+		return "#fff"
+	}
+	return "#000"
 }
diff --git a/modules/util/color_test.go b/modules/util/color_test.go
index d96ac36730..be6e6b122a 100644
--- a/modules/util/color_test.go
+++ b/modules/util/color_test.go
@@ -33,33 +33,31 @@ func Test_HexToRBGColor(t *testing.T) {
 	}
 }
 
-func Test_UseLightTextOnBackground(t *testing.T) {
+func Test_UseLightText(t *testing.T) {
 	cases := []struct {
-		r        float64
-		g        float64
-		b        float64
-		expected bool
+		color    string
+		expected string
 	}{
-		{215, 58, 74, true},
-		{0, 117, 202, true},
-		{207, 211, 215, false},
-		{162, 238, 239, false},
-		{112, 87, 255, true},
-		{0, 134, 114, true},
-		{228, 230, 105, false},
-		{216, 118, 227, true},
-		{255, 255, 255, false},
-		{43, 134, 133, true},
-		{43, 135, 134, true},
-		{44, 135, 134, true},
-		{59, 182, 179, true},
-		{124, 114, 104, true},
-		{126, 113, 108, true},
-		{129, 112, 109, true},
-		{128, 112, 112, true},
+		{"#d73a4a", "#fff"},
+		{"#0075ca", "#fff"},
+		{"#cfd3d7", "#000"},
+		{"#a2eeef", "#000"},
+		{"#7057ff", "#fff"},
+		{"#008672", "#fff"},
+		{"#e4e669", "#000"},
+		{"#d876e3", "#000"},
+		{"#ffffff", "#000"},
+		{"#2b8684", "#fff"},
+		{"#2b8786", "#fff"},
+		{"#2c8786", "#000"},
+		{"#3bb6b3", "#000"},
+		{"#7c7268", "#fff"},
+		{"#7e716c", "#fff"},
+		{"#81706d", "#fff"},
+		{"#807070", "#fff"},
+		{"#84b6eb", "#000"},
 	}
 	for n, c := range cases {
-		result := UseLightTextOnBackground(c.r, c.g, c.b)
-		assert.Equal(t, c.expected, result, "case %d: error should match", n)
+		assert.Equal(t, c.expected, ContrastColor(c.color), "case %d: error should match", n)
 	}
 }
diff --git a/modules/util/filebuffer/file_backed_buffer.go b/modules/util/filebuffer/file_backed_buffer.go
index 6b07bd0413..739543e297 100644
--- a/modules/util/filebuffer/file_backed_buffer.go
+++ b/modules/util/filebuffer/file_backed_buffer.go
@@ -149,6 +149,7 @@ func (b *FileBackedBuffer) Close() error {
 	if b.file != nil {
 		err := b.file.Close()
 		os.Remove(b.file.Name())
+		b.file = nil
 		return err
 	}
 	return nil
diff --git a/modules/util/keypair.go b/modules/util/keypair.go
index 97f2d9ebca..8b86c142af 100644
--- a/modules/util/keypair.go
+++ b/modules/util/keypair.go
@@ -7,10 +7,9 @@ import (
 	"crypto"
 	"crypto/rand"
 	"crypto/rsa"
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/pem"
-
-	"github.com/minio/sha256-simd"
 )
 
 // GenerateKeyPair generates a public and private keypair
diff --git a/modules/util/keypair_test.go b/modules/util/keypair_test.go
index c9925f7988..c6f68c845a 100644
--- a/modules/util/keypair_test.go
+++ b/modules/util/keypair_test.go
@@ -7,12 +7,12 @@ import (
 	"crypto"
 	"crypto/rand"
 	"crypto/rsa"
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/pem"
 	"regexp"
 	"testing"
 
-	"github.com/minio/sha256-simd"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/modules/util/slice.go b/modules/util/slice.go
index a7073fedee..9c878c24be 100644
--- a/modules/util/slice.go
+++ b/modules/util/slice.go
@@ -53,3 +53,21 @@ func Sorted[S ~[]E, E cmp.Ordered](values S) S {
 	slices.Sort(values)
 	return values
 }
+
+// TODO: Replace with "maps.Values" once available, current it only in golang.org/x/exp/maps but not in standard library
+func ValuesOfMap[K comparable, V any](m map[K]V) []V {
+	values := make([]V, 0, len(m))
+	for _, v := range m {
+		values = append(values, v)
+	}
+	return values
+}
+
+// TODO: Replace with "maps.Keys" once available, current it only in golang.org/x/exp/maps but not in standard library
+func KeysOfMap[K comparable, V any](m map[K]V) []K {
+	keys := make([]K, 0, len(m))
+	for k := range m {
+		keys = append(keys, k)
+	}
+	return keys
+}
diff --git a/modules/util/util.go b/modules/util/util.go
index c47931f6c9..44b5a6ed81 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -6,58 +6,24 @@ package util
 import (
 	"bytes"
 	"crypto/rand"
-	"encoding/base64"
 	"fmt"
 	"math/big"
 	"strconv"
 	"strings"
 
+	"code.gitea.io/gitea/modules/optional"
+
 	"golang.org/x/text/cases"
 	"golang.org/x/text/language"
 )
 
-// OptionalBool a boolean that can be "null"
-type OptionalBool byte
-
-const (
-	// OptionalBoolNone a "null" boolean value
-	OptionalBoolNone OptionalBool = iota
-	// OptionalBoolTrue a "true" boolean value
-	OptionalBoolTrue
-	// OptionalBoolFalse a "false" boolean value
-	OptionalBoolFalse
-)
-
-// IsTrue return true if equal to OptionalBoolTrue
-func (o OptionalBool) IsTrue() bool {
-	return o == OptionalBoolTrue
-}
-
-// IsFalse return true if equal to OptionalBoolFalse
-func (o OptionalBool) IsFalse() bool {
-	return o == OptionalBoolFalse
-}
-
-// IsNone return true if equal to OptionalBoolNone
-func (o OptionalBool) IsNone() bool {
-	return o == OptionalBoolNone
-}
-
-// OptionalBoolOf get the corresponding OptionalBool of a bool
-func OptionalBoolOf(b bool) OptionalBool {
-	if b {
-		return OptionalBoolTrue
-	}
-	return OptionalBoolFalse
-}
-
-// OptionalBoolParse get the corresponding OptionalBool of a string using strconv.ParseBool
-func OptionalBoolParse(s string) OptionalBool {
-	b, e := strconv.ParseBool(s)
+// OptionalBoolParse get the corresponding optional.Option[bool] of a string using strconv.ParseBool
+func OptionalBoolParse(s string) optional.Option[bool] {
+	v, e := strconv.ParseBool(s)
 	if e != nil {
-		return OptionalBoolNone
+		return optional.None[bool]()
 	}
-	return OptionalBoolOf(b)
+	return optional.Some(v)
 }
 
 // IsEmptyString checks if the provided string is empty
@@ -247,12 +213,28 @@ func ToPointer[T any](val T) *T {
 	return &val
 }
 
-func Base64FixedDecode(encoding *base64.Encoding, src []byte, length int) ([]byte, error) {
-	decoded := make([]byte, encoding.DecodedLen(len(src))+3)
-	if n, err := encoding.Decode(decoded, src); err != nil {
-		return nil, err
-	} else if n != length {
-		return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, length)
+// Iif is an "inline-if", it returns "trueVal" if "condition" is true, otherwise "falseVal"
+func Iif[T any](condition bool, trueVal, falseVal T) T {
+	if condition {
+		return trueVal
 	}
-	return decoded[:length], nil
+	return falseVal
+}
+
+// IfZero returns "def" if "v" is a zero value, otherwise "v"
+func IfZero[T comparable](v, def T) T {
+	var zero T
+	if v == zero {
+		return def
+	}
+	return v
+}
+
+func ReserveLineBreakForTextarea(input string) string {
+	// Since the content is from a form which is a textarea, the line endings are \r\n.
+	// It's a standard behavior of HTML.
+	// But we want to store them as \n like what GitHub does.
+	// And users are unlikely to really need to keep the \r.
+	// Other than this, we should respect the original content, even leading or trailing spaces.
+	return strings.ReplaceAll(input, "\r\n", "\n")
 }
diff --git a/modules/util/util_test.go b/modules/util/util_test.go
index 8509d8aced..5c5b13d04b 100644
--- a/modules/util/util_test.go
+++ b/modules/util/util_test.go
@@ -4,11 +4,12 @@
 package util
 
 import (
-	"encoding/base64"
 	"regexp"
 	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/modules/optional"
+
 	"github.com/stretchr/testify/assert"
 )
 
@@ -174,17 +175,17 @@ func Test_RandomBytes(t *testing.T) {
 	assert.NotEqual(t, bytes3, bytes4)
 }
 
-func Test_OptionalBool(t *testing.T) {
-	assert.Equal(t, OptionalBoolNone, OptionalBoolParse(""))
-	assert.Equal(t, OptionalBoolNone, OptionalBoolParse("x"))
+func TestOptionalBoolParse(t *testing.T) {
+	assert.Equal(t, optional.None[bool](), OptionalBoolParse(""))
+	assert.Equal(t, optional.None[bool](), OptionalBoolParse("x"))
 
-	assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("0"))
-	assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("f"))
-	assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("False"))
+	assert.Equal(t, optional.Some(false), OptionalBoolParse("0"))
+	assert.Equal(t, optional.Some(false), OptionalBoolParse("f"))
+	assert.Equal(t, optional.Some(false), OptionalBoolParse("False"))
 
-	assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("1"))
-	assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("t"))
-	assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("True"))
+	assert.Equal(t, optional.Some(true), OptionalBoolParse("1"))
+	assert.Equal(t, optional.Some(true), OptionalBoolParse("t"))
+	assert.Equal(t, optional.Some(true), OptionalBoolParse("True"))
 }
 
 // Test case for any function which accepts and returns a single string.
@@ -235,15 +236,7 @@ func TestToPointer(t *testing.T) {
 	assert.False(t, &val123 == ToPointer(val123))
 }
 
-func TestBase64FixedDecode(t *testing.T) {
-	_, err := Base64FixedDecode(base64.RawURLEncoding, []byte("abcd"), 32)
-	assert.ErrorContains(t, err, "invalid base64 decoded length")
-	_, err = Base64FixedDecode(base64.RawURLEncoding, []byte(strings.Repeat("a", 64)), 32)
-	assert.ErrorContains(t, err, "invalid base64 decoded length")
-
-	str32 := strings.Repeat("x", 32)
-	encoded32 := base64.RawURLEncoding.EncodeToString([]byte(str32))
-	decoded32, err := Base64FixedDecode(base64.RawURLEncoding, []byte(encoded32), 32)
-	assert.NoError(t, err)
-	assert.Equal(t, str32, string(decoded32))
+func TestReserveLineBreakForTextarea(t *testing.T) {
+	assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata"), "test\ndata")
+	assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata\r\n"), "test\ndata\n")
 }
diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go
index d9bcdf3b2a..43e1bbc70e 100644
--- a/modules/web/middleware/binding.go
+++ b/modules/web/middleware/binding.go
@@ -104,40 +104,40 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
 
 			trName := field.Tag.Get("locale")
 			if len(trName) == 0 {
-				trName = l.Tr("form." + field.Name)
+				trName = l.TrString("form." + field.Name)
 			} else {
-				trName = l.Tr(trName)
+				trName = l.TrString(trName)
 			}
 
 			switch errs[0].Classification {
 			case binding.ERR_REQUIRED:
-				data["ErrorMsg"] = trName + l.Tr("form.require_error")
+				data["ErrorMsg"] = trName + l.TrString("form.require_error")
 			case binding.ERR_ALPHA_DASH:
-				data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error")
+				data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error")
 			case binding.ERR_ALPHA_DASH_DOT:
-				data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_error")
+				data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error")
 			case validation.ErrGitRefName:
-				data["ErrorMsg"] = trName + l.Tr("form.git_ref_name_error")
+				data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error")
 			case binding.ERR_SIZE:
-				data["ErrorMsg"] = trName + l.Tr("form.size_error", GetSize(field))
+				data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field))
 			case binding.ERR_MIN_SIZE:
-				data["ErrorMsg"] = trName + l.Tr("form.min_size_error", GetMinSize(field))
+				data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field))
 			case binding.ERR_MAX_SIZE:
-				data["ErrorMsg"] = trName + l.Tr("form.max_size_error", GetMaxSize(field))
+				data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field))
 			case binding.ERR_EMAIL:
-				data["ErrorMsg"] = trName + l.Tr("form.email_error")
+				data["ErrorMsg"] = trName + l.TrString("form.email_error")
 			case binding.ERR_URL:
-				data["ErrorMsg"] = trName + l.Tr("form.url_error", errs[0].Message)
+				data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message)
 			case binding.ERR_INCLUDE:
-				data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field))
+				data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field))
 			case validation.ErrGlobPattern:
-				data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message)
+				data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message)
 			case validation.ErrRegexPattern:
-				data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
+				data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message)
 			case validation.ErrUsername:
-				data["ErrorMsg"] = trName + l.Tr("form.username_error")
+				data["ErrorMsg"] = trName + l.TrString("form.username_error")
 			case validation.ErrInvalidGroupTeamMap:
-				data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
+				data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
 			default:
 				msg := errs[0].Classification
 				if msg != "" && errs[0].Message != "" {
@@ -146,7 +146,7 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
 
 				msg += errs[0].Message
 				if msg == "" {
-					msg = l.Tr("form.unknown_error")
+					msg = l.TrString("form.unknown_error")
 				}
 				data["ErrorMsg"] = trName + ": " + msg
 			}
diff --git a/modules/web/middleware/flash.go b/modules/web/middleware/flash.go
index 41f3aac27c..88da2049a4 100644
--- a/modules/web/middleware/flash.go
+++ b/modules/web/middleware/flash.go
@@ -3,7 +3,11 @@
 
 package middleware
 
-import "net/url"
+import (
+	"fmt"
+	"html/template"
+	"net/url"
+)
 
 // Flash represents a one time data transfer between two requests.
 type Flash struct {
@@ -26,26 +30,36 @@ func (f *Flash) set(name, msg string, current ...bool) {
 	}
 }
 
+func flashMsgStringOrHTML(msg any) string {
+	switch v := msg.(type) {
+	case string:
+		return v
+	case template.HTML:
+		return string(v)
+	}
+	panic(fmt.Sprintf("unknown type: %T", msg))
+}
+
 // Error sets error message
-func (f *Flash) Error(msg string, current ...bool) {
-	f.ErrorMsg = msg
-	f.set("error", msg, current...)
+func (f *Flash) Error(msg any, current ...bool) {
+	f.ErrorMsg = flashMsgStringOrHTML(msg)
+	f.set("error", f.ErrorMsg, current...)
 }
 
 // Warning sets warning message
-func (f *Flash) Warning(msg string, current ...bool) {
-	f.WarningMsg = msg
-	f.set("warning", msg, current...)
+func (f *Flash) Warning(msg any, current ...bool) {
+	f.WarningMsg = flashMsgStringOrHTML(msg)
+	f.set("warning", f.WarningMsg, current...)
 }
 
 // Info sets info message
-func (f *Flash) Info(msg string, current ...bool) {
-	f.InfoMsg = msg
-	f.set("info", msg, current...)
+func (f *Flash) Info(msg any, current ...bool) {
+	f.InfoMsg = flashMsgStringOrHTML(msg)
+	f.set("info", f.InfoMsg, current...)
 }
 
 // Success sets success message
-func (f *Flash) Success(msg string, current ...bool) {
-	f.SuccessMsg = msg
-	f.set("success", msg, current...)
+func (f *Flash) Success(msg any, current ...bool) {
+	f.SuccessMsg = flashMsgStringOrHTML(msg)
+	f.set("success", f.SuccessMsg, current...)
 }
diff --git a/options/license/AMD-newlib b/options/license/AMD-newlib
new file mode 100644
index 0000000000..1b2f1abd6f
--- /dev/null
+++ b/options/license/AMD-newlib
@@ -0,0 +1,11 @@
+Copyright 1989, 1990 Advanced Micro Devices, Inc.
+
+This software is the property of Advanced Micro Devices, Inc  (AMD)  which
+specifically  grants the user the right to modify, use and distribute this
+software provided this notice is not removed or altered.  All other rights
+are reserved by AMD.
+
+AMD MAKES NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, WITH REGARD TO THIS
+SOFTWARE.  IN NO EVENT SHALL AMD BE LIABLE FOR INCIDENTAL OR CONSEQUENTIAL
+DAMAGES IN CONNECTION WITH OR ARISING FROM THE FURNISHING, PERFORMANCE, OR
+USE OF THIS SOFTWARE.
diff --git a/options/license/APL-1.0 b/options/license/APL-1.0
index 261f2d687c..0748f90cd9 100644
--- a/options/license/APL-1.0
+++ b/options/license/APL-1.0
@@ -210,21 +210,21 @@ PART 1: INITIAL CONTRIBUTOR AND DESIGNATED WEB SITE
 
 The Initial Contributor is:
 ____________________________________________________
- 
+ 
 [Enter full name of Initial Contributor]
 
 Address of Initial Contributor:
 ________________________________________________
- 
+ 
 ________________________________________________
- 
+ 
 ________________________________________________
- 
+ 
 [Enter address above]
 
 The Designated Web Site is:
 __________________________________________________
- 
+ 
 [Enter URL for Designated Web Site of Initial Contributor]
 
 NOTE: The Initial Contributor is to complete this Part 1, along with Parts 2, 3, and 5, and, if applicable, Parts 4 and 6.
@@ -237,27 +237,27 @@ The date on which the Initial Work was first available under this License: _____
 
 PART 3: GOVERNING JURISDICTION
 
-For the purposes of this License, the Governing Jurisdiction is _________________________________________________. 
[Initial Contributor to Enter Governing Jurisdiction here]
+For the purposes of this License, the Governing Jurisdiction is _________________________________________________. [Initial Contributor to Enter Governing Jurisdiction here]
 
 PART 4: THIRD PARTIES
 
 For the purposes of this License, "Third Party" has the definition set forth below in the ONE paragraph selected by the Initial Contributor from paragraphs A, B, C, D and E when the Initial Work is distributed or otherwise made available by the Initial Contributor. To select one of the following paragraphs, the Initial Contributor must place an "X" or "x" in the selection box alongside the one respective paragraph selected.
 SELECTION
- 
+ 
 BOX   PARAGRAPH
-[  ]  A. "THIRD PARTY" means any third party.
- 
- 
-[  ]  B. "THIRD PARTY" means any third party except for any of the following: (a) a wholly owned subsidiary of the Subsequent Contributor in question; (b) a legal entity (the "PARENT") that wholly owns the Subsequent Contributor in question; or (c) a wholly owned subsidiary of the wholly owned subsidiary in (a) or of the Parent in (b).
- 
- 
-[  ]  C. "THIRD PARTY" means any third party except for any of the following: (a) any Person directly or indirectly owning a majority of the voting interest in the Subsequent Contributor or (b) any Person in which the Subsequent Contributor directly or indirectly owns a majority voting interest.
- 
- 
-[  ]  D. "THIRD PARTY" means any third party except for any Person directly or indirectly controlled by the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
- 
- 
-[  ]  E. "THIRD PARTY" means any third party except for any Person directly or indirectly controlling, controlled by, or under common control with the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
+[  ]  A. "THIRD PARTY" means any third party.
+ 
+ 
+[  ]  B. "THIRD PARTY" means any third party except for any of the following: (a) a wholly owned subsidiary of the Subsequent Contributor in question; (b) a legal entity (the "PARENT") that wholly owns the Subsequent Contributor in question; or (c) a wholly owned subsidiary of the wholly owned subsidiary in (a) or of the Parent in (b).
+ 
+ 
+[  ]  C. "THIRD PARTY" means any third party except for any of the following: (a) any Person directly or indirectly owning a majority of the voting interest in the Subsequent Contributor or (b) any Person in which the Subsequent Contributor directly or indirectly owns a majority voting interest.
+ 
+ 
+[  ]  D. "THIRD PARTY" means any third party except for any Person directly or indirectly controlled by the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
+ 
+ 
+[  ]  E. "THIRD PARTY" means any third party except for any Person directly or indirectly controlling, controlled by, or under common control with the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
 The default definition of "THIRD PARTY" is the definition set forth in paragraph A, if NONE OR MORE THAN ONE of paragraphs A, B, C, D or E in this Part 4 are selected by the Initial Contributor.
 
 PART 5: NOTICE
@@ -271,8 +271,8 @@ PART 6: PATENT LICENSING TERMS
 For the purposes of this License, paragraphs A, B, C, D and E of this Part 6 of Exhibit A are only incorporated and form part of the terms of the License if the Initial Contributor places an "X" or "x" in the selection box alongside the YES answer to the question immediately below.
 
 Is this a Patents-Included License pursuant to Section 2.2 of the License?
-YES   [      ]
-NO    [      ]
+YES   [      ]
+NO    [      ]
 
 By default, if YES is not selected by the Initial Contributor, the answer is NO.
 
diff --git a/options/license/Brian-Gladman-2-Clause b/options/license/Brian-Gladman-2-Clause
new file mode 100644
index 0000000000..7276f63e9e
--- /dev/null
+++ b/options/license/Brian-Gladman-2-Clause
@@ -0,0 +1,17 @@
+Copyright (C) 1998-2013, Brian Gladman, Worcester, UK. All
+   rights reserved.
+
+The redistribution and use of this software (with or without
+changes) is allowed without the payment of fees or royalties
+provided that:
+
+   source code distributions include the above copyright notice,
+   this list of conditions and the following disclaimer;
+
+   binary distributions include the above copyright notice, this
+   list of conditions and the following disclaimer in their
+   documentation.
+
+This software is provided 'as is' with no explicit or implied
+warranties in respect of its operation, including, but not limited
+to, correctness and fitness for purpose.
diff --git a/options/license/CMU-Mach-nodoc b/options/license/CMU-Mach-nodoc
new file mode 100644
index 0000000000..c81d74fee7
--- /dev/null
+++ b/options/license/CMU-Mach-nodoc
@@ -0,0 +1,11 @@
+Copyright (C) 2002 Naval Research Laboratory (NRL/CCS)
+
+Permission to use, copy, modify and distribute this software and
+its documentation is hereby granted, provided that both the
+copyright notice and this permission notice appear in all copies of
+the software, derivative works or modified versions, and any
+portions thereof.
+
+NRL ALLOWS FREE USE OF THIS SOFTWARE IN ITS "AS IS" CONDITION AND
+DISCLAIMS ANY LIABILITY OF ANY KIND FOR ANY DAMAGES WHATSOEVER
+RESULTING FROM THE USE OF THIS SOFTWARE.
diff --git a/options/license/GNOME-examples-exception b/options/license/GNOME-examples-exception
new file mode 100644
index 0000000000..0f0cd53b50
--- /dev/null
+++ b/options/license/GNOME-examples-exception
@@ -0,0 +1 @@
+As a special exception, the copyright holders give you permission to copy, modify, and distribute the example code contained in this document under the terms of your choosing, without restriction.
diff --git a/options/license/Gmsh-exception b/options/license/Gmsh-exception
new file mode 100644
index 0000000000..6d28f704e4
--- /dev/null
+++ b/options/license/Gmsh-exception
@@ -0,0 +1,16 @@
+The copyright holders of Gmsh give you permission to combine Gmsh
+  with code included in the standard release of Netgen (from Joachim
+  Sch"oberl), METIS (from George Karypis at the University of
+  Minnesota), OpenCASCADE (from Open CASCADE S.A.S) and ParaView
+  (from Kitware, Inc.) under their respective licenses. You may copy
+  and distribute such a system following the terms of the GNU GPL for
+  Gmsh and the licenses of the other code concerned, provided that
+  you include the source code of that other code when and as the GNU
+  GPL requires distribution of source code.
+
+  Note that people who make modified versions of Gmsh are not
+  obligated to grant this special exception for their modified
+  versions; it is their choice whether to do so. The GNU General
+  Public License gives permission to release a modified version
+  without this exception; this exception also makes it possible to
+  release a modified version which carries forward this exception.
diff --git a/options/license/HPND-Fenneberg-Livingston b/options/license/HPND-Fenneberg-Livingston
new file mode 100644
index 0000000000..aaf524f3aa
--- /dev/null
+++ b/options/license/HPND-Fenneberg-Livingston
@@ -0,0 +1,13 @@
+Copyright (C) 1995,1996,1997,1998 Lars Fenneberg <lf@elemental.net>
+
+Permission to use, copy, modify, and distribute this software for any
+purpose and without fee is hereby granted, provided that this copyright and
+permission notice appear on all copies and supporting documentation, the
+name of Lars Fenneberg not be used in advertising or publicity pertaining to
+distribution of the program without specific prior permission, and notice be
+given in supporting documentation that copying and distribution is by
+permission of Lars Fenneberg.
+
+Lars Fenneberg makes no representations about the suitability of this
+software for any purpose.  It is provided "as is" without express or implied
+warranty.
diff --git a/options/license/HPND-INRIA-IMAG b/options/license/HPND-INRIA-IMAG
new file mode 100644
index 0000000000..87d09d92cb
--- /dev/null
+++ b/options/license/HPND-INRIA-IMAG
@@ -0,0 +1,9 @@
+This software is available with usual "research" terms with
+the aim of retain credits of the software. Permission to use,
+copy, modify and distribute this software for any purpose and
+without fee is hereby granted, provided that the above copyright
+notice and this permission notice appear in all copies, and
+the name of INRIA, IMAG, or any contributor not be used in
+advertising or publicity pertaining to this material without
+the prior explicit permission. The software is provided "as
+is" without any warranties, support or liabilities of any kind.
diff --git a/options/license/IBM-pibs b/options/license/IBM-pibs
index 49454b8b1e..ee9c7be36d 100644
--- a/options/license/IBM-pibs
+++ b/options/license/IBM-pibs
@@ -4,5 +4,5 @@ Any user of this software should understand that IBM cannot provide technical su
 
 Any person who transfers this source code or any derivative work must include the IBM copyright notice, this paragraph, and the preceding two paragraphs in the transferred software.
 
-COPYRIGHT   I B M   CORPORATION 2002
-LICENSED MATERIAL  -  PROGRAM PROPERTY OF I B M
+COPYRIGHT   I B M   CORPORATION 2002
+LICENSED MATERIAL  -  PROGRAM PROPERTY OF I B M
diff --git a/options/license/MIT-Khronos-old b/options/license/MIT-Khronos-old
new file mode 100644
index 0000000000..430863bc98
--- /dev/null
+++ b/options/license/MIT-Khronos-old
@@ -0,0 +1,23 @@
+Copyright (c) 2014-2020 The Khronos Group Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and/or associated documentation files (the "Materials"),
+to deal in the Materials without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Materials, and to permit persons to whom the
+Materials are 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 Materials.
+
+MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS KHRONOS
+STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS SPECIFICATIONS AND
+HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/
+
+THE MATERIALS ARE 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 MATERIALS OR THE USE OR OTHER DEALINGS
+IN THE MATERIALS.
diff --git a/options/license/Mackerras-3-Clause b/options/license/Mackerras-3-Clause
new file mode 100644
index 0000000000..6467f0c98e
--- /dev/null
+++ b/options/license/Mackerras-3-Clause
@@ -0,0 +1,25 @@
+Copyright (c) 1995 Eric Rosenquist.  All rights reserved.
+  
+   Redistribution and use in source and binary forms, with or without
+   modification, are permitted provided that the following conditions
+   are met:
+  
+   1. Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+  
+   2. Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in
+      the documentation and/or other materials provided with the
+      distribution.
+  
+   3. The name(s) of the authors of this software must not be used to
+      endorse or promote products derived from this software without
+      prior written permission.
+  
+   THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO
+   THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+   AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
+   SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+   WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+   AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+   OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/options/license/Mackerras-3-Clause-acknowledgment b/options/license/Mackerras-3-Clause-acknowledgment
new file mode 100644
index 0000000000..5f0187add7
--- /dev/null
+++ b/options/license/Mackerras-3-Clause-acknowledgment
@@ -0,0 +1,25 @@
+Copyright (c) 1993-2002 Paul Mackerras. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 
+ 1. Redistributions of source code must retain the above copyright
+     notice, this list of conditions and the following disclaimer.
+ 
+2. The name(s) of the authors of this software must not be used to
+   endorse or promote products derived from this software without
+   prior written permission.
+
+3. Redistributions of any form whatsoever must retain the following
+   acknowledgment:
+   "This product includes software developed by Paul Mackerras
+   <paulus@ozlabs.org>".
+
+THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
+SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/options/license/NCGL-UK-2.0 b/options/license/NCGL-UK-2.0
index 31fbad6f83..15c4f63c22 100644
--- a/options/license/NCGL-UK-2.0
+++ b/options/license/NCGL-UK-2.0
@@ -12,15 +12,15 @@ The Licensor grants you a worldwide, royalty-free, perpetual, non-exclusive lice
 This licence does not affect your freedom under fair dealing or fair use or any other copyright or database right exceptions and limitations.
 
 You are free to:
-		copy, publish, distribute and transmit the Information;
+		copy, publish, distribute and transmit the Information;
 		adapt the Information;
 		exploit the Information for Non-Commercial purposes for example, by combining it with other information in your own product or application.
 
 You are not permitted to:
-		exercise any of the rights granted to you by this licence in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation.
+		exercise any of the rights granted to you by this licence in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation.
 
 You must, where you do any of the above:
-		acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
+		acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
 
 If the Information Provider does not provide a specific attribution statement, you must use the following:
    Contains information licensed under the Non-Commercial Government Licence v2.0.
diff --git a/options/license/NPL-1.1 b/options/license/NPL-1.1
index 62c5296400..0d5457ff04 100644
--- a/options/license/NPL-1.1
+++ b/options/license/NPL-1.1
@@ -2,7 +2,7 @@ Netscape Public LIcense version 1.1
 
 AMENDMENTS
 
-The Netscape Public License Version 1.1 ("NPL") consists of the Mozilla Public License Version 1.1 with the following Amendments, including Exhibit A-Netscape Public License.  Files identified with "Exhibit A-Netscape Public License" are governed by the Netscape Public License Version 1.1.
+The Netscape Public License Version 1.1 ("NPL") consists of the Mozilla Public License Version 1.1 with the following Amendments, including Exhibit A-Netscape Public License.  Files identified with "Exhibit A-Netscape Public License" are governed by the Netscape Public License Version 1.1.
 
 Additional Terms applicable to the Netscape Public License.
 
@@ -28,7 +28,7 @@ Additional Terms applicable to the Netscape Public License.
      Notwithstanding the limitations of Section 11 above, the provisions regarding litigation in Section 11(a), (b) and (c) of the License shall apply to all disputes relating to this License.
 
 	 EXHIBIT A-Netscape Public License.
-	 
+	 
 "The contents of this file are subject to the Netscape Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/NPL/
 
 Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License.
@@ -37,8 +37,8 @@ The Original Code is Mozilla Communicator client code, released March 31, 1998.
 
 The Initial Developer of the Original Code is Netscape Communications Corporation. Portions created by Netscape are Copyright (C) 1998-1999 Netscape Communications Corporation. All Rights Reserved.
 Contributor(s): ______________________________________.
-	 
-Alternatively, the contents of this file may be used under the terms of the _____ license (the  "[___] License"), in which case the provisions of [______] License are applicable  instead of those above.  If you wish to allow use of your version of this file only under the terms of the [____] License and not to allow others to use your version of this file under the NPL, indicate your decision by deleting  the provisions above and replace  them with the notice and other provisions required by the [___] License.  If you do not delete the provisions above, a recipient may use your version of this file under either the NPL or the [___] License."
+	 
+Alternatively, the contents of this file may be used under the terms of the _____ license (the  "[___] License"), in which case the provisions of [______] License are applicable  instead of those above.  If you wish to allow use of your version of this file only under the terms of the [____] License and not to allow others to use your version of this file under the NPL, indicate your decision by deleting  the provisions above and replace  them with the notice and other provisions required by the [___] License.  If you do not delete the provisions above, a recipient may use your version of this file under either the NPL or the [___] License."
 
 
 Mozilla Public License Version 1.1
diff --git a/options/license/OAR b/options/license/OAR
new file mode 100644
index 0000000000..ca5c4b9617
--- /dev/null
+++ b/options/license/OAR
@@ -0,0 +1,12 @@
+COPYRIGHT (c) 1989-2013, 2015.
+On-Line Applications Research Corporation (OAR).
+
+Permission to use, copy, modify, and distribute this software for any
+purpose without fee is hereby granted, provided that this entire notice
+is included in all copies of any software which is or includes a copy
+or modification of this software.
+
+THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
+WARRANTY.  IN PARTICULAR,  THE AUTHOR MAKES NO REPRESENTATION
+OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY OF THIS
+SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE.
diff --git a/options/license/OCCT-PL b/options/license/OCCT-PL
index 85df3c73c5..9b6fccc1c9 100644
--- a/options/license/OCCT-PL
+++ b/options/license/OCCT-PL
@@ -6,7 +6,7 @@ OPEN CASCADE releases and makes publicly available the source code of the softwa
 It is not the purpose of this license to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this license has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
 
 Please read this license carefully and completely before downloading this software. By downloading, using, modifying, distributing and sublicensing this software, you indicate your acceptance to be bound by the terms and conditions of this license. If you do not want to accept or cannot accept for any reasons the terms and conditions of this license, please do not download or use in any manner this software.
- 
+ 
 1. Definitions
 
 Unless there is something in the subject matter or in the context inconsistent therewith, the capitalized terms used in this License shall have the following meaning.
@@ -26,13 +26,13 @@ Unless there is something in the subject matter or in the context inconsistent t
 "Software": means the Original Code, the Modifications, the combination of Original Code and any Modifications or any respective portions thereof.
 
 "You" or "Your": means an individual or a legal entity exercising rights under this License
- 
+ 
 2. Acceptance of license
 By using, reproducing, modifying, distributing or sublicensing the Software or any portion thereof, You expressly indicate Your acceptance of the terms and conditions of this License and undertake to act in accordance with all the provisions of this License applicable to You.
- 
+ 
 3. Scope and purpose
 This License applies to the Software and You may not use, reproduce, modify, distribute, sublicense or circulate the Software, or any portion thereof, except as expressly provided under this License. Any attempt to otherwise use, reproduce, modify, distribute or sublicense the Software is void and will automatically terminate Your rights under this License.
- 
+ 
 4. Contributor license
 Subject to the terms and conditions of this License, the Initial Developer and each of the Contributors hereby grant You a world-wide, royalty-free, irrevocable and non-exclusive license under the Applicable Intellectual Property Rights they own or control, to use, reproduce, modify, distribute and sublicense the Software provided that:
 
diff --git a/options/license/OGL-UK-1.0 b/options/license/OGL-UK-1.0
index a761c9916f..867c0e353b 100644
--- a/options/license/OGL-UK-1.0
+++ b/options/license/OGL-UK-1.0
@@ -10,20 +10,20 @@ The Licensor grants you a worldwide, royalty-free, perpetual, non-exclusive lice
 This licence does not affect your freedom under fair dealing or fair use or any other copyright or database right exceptions and limitations.
 
 You are free to:
-		copy, publish, distribute and transmit the Information;
+		copy, publish, distribute and transmit the Information;
 		adapt the Information;
 		exploit the Information commercially for example, by combining it with other Information, or by including it in your own product or application.
 
 You must, where you do any of the above:
-		acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
-		 If the Information Provider does not provide a specific attribution statement, or if you are using Information from several Information Providers and multiple attributions are not practical in your product or application, you may consider using the following:
 Contains public sector information licensed under the Open Government Licence v1.0.
+		acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
+		 If the Information Provider does not provide a specific attribution statement, or if you are using Information from several Information Providers and multiple attributions are not practical in your product or application, you may consider using the following: Contains public sector information licensed under the Open Government Licence v1.0.
 		ensure that you do not use the Information in a way that suggests any official status or that the Information Provider endorses you or your use of the Information;
 		ensure that you do not mislead others or misrepresent the Information or its source;
 		ensure that your use of the Information does not breach the Data Protection Act 1998 or the Privacy and Electronic Communications (EC Directive) Regulations 2003.
 
 These are important conditions of this licence and if you fail to comply with them the rights granted to you under this licence, or any similar licence granted by the Licensor, will end automatically.
 
- Exemptions
+ Exemptions
 
 This licence does not cover the use of:
 	- personal data in the Information;
@@ -48,22 +48,22 @@ Definitions
 
 In this licence, the terms below have the following meanings:
 
-‘Information’
means information protected by copyright or by database right (for example, literary and artistic works, content, data and source code) offered for use under the terms of this licence.
+‘Information’ means information protected by copyright or by database right (for example, literary and artistic works, content, data and source code) offered for use under the terms of this licence.
 
-‘Information Provider’
means the person or organisation providing the Information under this licence.
+‘Information Provider’ means the person or organisation providing the Information under this licence.
 
-‘Licensor’
means any Information Provider which has the authority to offer Information under the terms of this licence or the Controller of Her Majesty’s Stationery Office, who has the authority to offer Information subject to Crown copyright and Crown database rights and Information subject to copyright and database right that has been assigned to or acquired by the Crown, under the terms of this licence.
+‘Licensor’ means any Information Provider which has the authority to offer Information under the terms of this licence or the Controller of Her Majesty’s Stationery Office, who has the authority to offer Information subject to Crown copyright and Crown database rights and Information subject to copyright and database right that has been assigned to or acquired by the Crown, under the terms of this licence.
 
-‘Use’
as a verb, means doing any act which is restricted by copyright or database right, whether in the original medium or in any other medium, and includes without limitation distributing, copying, adapting, modifying as may be technically necessary to use it in a different mode or format.
+‘Use’ as a verb, means doing any act which is restricted by copyright or database right, whether in the original medium or in any other medium, and includes without limitation distributing, copying, adapting, modifying as may be technically necessary to use it in a different mode or format.
 
-‘You’
means the natural or legal person, or body of persons corporate or incorporate, acquiring rights under this licence.
+‘You’ means the natural or legal person, or body of persons corporate or incorporate, acquiring rights under this licence.
 
 About the Open Government Licence
 The Controller of Her Majesty’s Stationery Office (HMSO) has developed this licence as a tool to enable Information Providers in the public sector to license the use and re-use of their Information under a common open licence. The Controller invites public sector bodies owning their own copyright and database rights to permit the use of their Information under this licence.
 
-The Controller of HMSO has authority to license Information subject to copyright and database right owned by the Crown. The extent of the Controller’s offer to license this Information under the terms of this licence is set out in the UK Government Licensing Framework.
+The Controller of HMSO has authority to license Information subject to copyright and database right owned by the Crown. The extent of the Controller’s offer to license this Information under the terms of this licence is set out in the UK Government Licensing Framework.
 
 This is version 1.0 of the Open Government Licence. The Controller of HMSO may, from time to time, issue new versions of the Open Government Licence. However, you may continue to use Information licensed under this version should you wish to do so.
 These terms have been aligned to be interoperable with any Creative Commons Attribution Licence, which covers copyright, and Open Data Commons Attribution License, which covers database rights and applicable copyrights.
 
-Further context, best practice and guidance can be found in the UK Government Licensing Framework section on The National Archives website.
+Further context, best practice and guidance can be found in the UK Government Licensing Framework section on The National Archives website.
diff --git a/options/license/OSET-PL-2.1 b/options/license/OSET-PL-2.1
index 15f0c7758c..e0ed2e1398 100644
--- a/options/license/OSET-PL-2.1
+++ b/options/license/OSET-PL-2.1
@@ -100,7 +100,8 @@ If it is impossible for You to comply with any of the terms of this License with
      5.1 Failure to Comply
 	 The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60-days after You have come back into compliance.  Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30-days after Your receipt of the notice.
 
-     5.2 Patent Infringement Claims
     If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.
+     5.2 Patent Infringement Claims
+	 If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.
 
      5.3 Additional Compliance Terms
 	 Notwithstanding the foregoing in this Section 5, for purposes of this Section, if You breach Section 3.1 (Distribution of Source Form), Section 3.2 (Distribution of Executable Form), Section 3.3 (Distribution of a Larger Work), or Section 3.4 (Notices), then becoming compliant as described in Section 5.1 must also include, no later than 30 days after receipt by You of notice of such violation by a Contributor, making the Covered Software available in Source Code Form as required by this License on a publicly available computer network for a period of no less than three (3) years.
diff --git a/options/license/OpenVision b/options/license/OpenVision
new file mode 100644
index 0000000000..983505389e
--- /dev/null
+++ b/options/license/OpenVision
@@ -0,0 +1,33 @@
+Copyright, OpenVision Technologies, Inc., 1993-1996, All Rights
+Reserved
+
+WARNING:  Retrieving the OpenVision Kerberos Administration system
+source code, as described below, indicates your acceptance of the
+following terms.  If you do not agree to the following terms, do
+not retrieve the OpenVision Kerberos administration system.
+
+You may freely use and distribute the Source Code and Object Code
+compiled from it, with or without modification, but this Source
+Code is provided to you "AS IS" EXCLUSIVE OF ANY WARRANTY,
+INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY OR
+FITNESS FOR A PARTICULAR PURPOSE, OR ANY OTHER WARRANTY, WHETHER
+EXPRESS OR IMPLIED. IN NO EVENT WILL OPENVISION HAVE ANY LIABILITY
+FOR ANY LOST PROFITS, LOSS OF DATA OR COSTS OF PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES, OR FOR ANY SPECIAL, INDIRECT, OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THIS AGREEMENT, INCLUDING,
+WITHOUT LIMITATION, THOSE RESULTING FROM THE USE OF THE SOURCE
+CODE, OR THE FAILURE OF THE SOURCE CODE TO PERFORM, OR FOR ANY
+OTHER REASON.
+
+OpenVision retains all copyrights in the donated Source Code.
+OpenVision also retains copyright to derivative works of the Source
+Code, whether created by OpenVision or by a third party. The
+OpenVision copyright notice must be preserved if derivative works
+are made based on the donated Source Code.
+
+OpenVision Technologies, Inc. has donated this Kerberos
+Administration system to MIT for inclusion in the standard Kerberos
+5 distribution. This donation underscores our commitment to
+continuing Kerberos technology development and our gratitude for
+the valuable work which has been performed by MIT and the Kerberos
+community.
diff --git a/options/license/SHL-2.0 b/options/license/SHL-2.0
index e522a396fe..9218b47a72 100644
--- a/options/license/SHL-2.0
+++ b/options/license/SHL-2.0
@@ -1,22 +1,22 @@
 # Solderpad Hardware Licence Version 2.0
 
-This licence (the “Licence”) operates as a wraparound licence to the Apache License Version 2.0 (the “Apache License”) and grants to You the rights, and imposes the obligations, set out in the Apache License (which can be found here: http://apache.org/licenses/LICENSE-2.0), with the following extensions. It must be read in conjunction with the Apache License. Section 1 below modifies definitions in the Apache License, and section 2 below replaces sections 2 of the Apache License. You may, at your option, choose to treat any Work released under this License as released under the Apache License (thus ignoring all sections written below entirely). Words in italics indicate changes rom the Apache License, but are indicative and not to be taken into account in interpretation.
+This licence (the “Licence”) operates as a wraparound licence to the Apache License Version 2.0 (the “Apache License”) and grants to You the rights, and imposes the obligations, set out in the Apache License (which can be found here: http://apache.org/licenses/LICENSE-2.0), with the following extensions. It must be read in conjunction with the Apache License. Section 1 below modifies definitions in the Apache License, and section 2 below replaces sections 2 of the Apache License. You may, at your option, choose to treat any Work released under this License as released under the Apache License (thus ignoring all sections written below entirely). Words in italics indicate changes rom the Apache License, but are indicative and not to be taken into account in interpretation.
 
 1. The definitions set out in the Apache License are modified as follows:
 
-Copyright any reference to ‘copyright’ (whether capitalised or not) includes ‘Rights’ (as defined below).
+Copyright any reference to ‘copyright’ (whether capitalised or not) includes ‘Rights’ (as defined below).
 
-Contribution also includes any design, as well as any work of authorship.
+Contribution also includes any design, as well as any work of authorship.
 
-Derivative Works shall not include works that remain reversibly separable from, or merely link (or bind by name) or physically connect to or interoperate with the interfaces of the Work and Derivative Works thereof.
+Derivative Works shall not include works that remain reversibly separable from, or merely link (or bind by name) or physically connect to or interoperate with the interfaces of the Work and Derivative Works thereof.
 
-Object form shall mean any form resulting from mechanical transformation or translation of a Source form or the application of a Source form to physical material, including but not limited to compiled object code, generated documentation, the instantiation of a hardware design or physical object and conversions to other media types, including intermediate forms such as bytecodes, FPGA bitstreams, moulds, artwork and semiconductor topographies (mask works).
+Object form shall mean any form resulting from mechanical transformation or translation of a Source form or the application of a Source form to physical material, including but not limited to compiled object code, generated documentation, the instantiation of a hardware design or physical object and conversions to other media types, including intermediate forms such as bytecodes, FPGA bitstreams, moulds, artwork and semiconductor topographies (mask works).
 
-Rights means copyright and any similar right including design right (whether registered or unregistered), semiconductor topography (mask) rights and database rights (but excluding Patents and Trademarks).
+Rights means copyright and any similar right including design right (whether registered or unregistered), semiconductor topography (mask) rights and database rights (but excluding Patents and Trademarks).
 
-Source form shall mean the preferred form for making modifications, including but not limited to source code, net lists, board layouts, CAD files, documentation source, and configuration files.
-Work also includes a design or work of authorship, whether in Source form or other Object form.
+Source form shall mean the preferred form for making modifications, including but not limited to source code, net lists, board layouts, CAD files, documentation source, and configuration files.
+Work also includes a design or work of authorship, whether in Source form or other Object form.
 
 2. Grant of Licence
 
-2.1 Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license under the Rights to reproduce, prepare Derivative Works of, make, adapt, repair, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form and do anything in relation to the Work as if the Rights did not exist.
+2.1 Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license under the Rights to reproduce, prepare Derivative Works of, make, adapt, repair, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form and do anything in relation to the Work as if the Rights did not exist.
diff --git a/options/license/SHL-2.1 b/options/license/SHL-2.1
index 4815a9e5ed..c9ae53741f 100644
--- a/options/license/SHL-2.1
+++ b/options/license/SHL-2.1
@@ -19,7 +19,7 @@ The following definitions shall replace the corresponding definitions in the Apa
 "License" shall mean this Solderpad Hardware License version 2.1, being the terms and conditions for use, manufacture, instantiation, adaptation, reproduction, and distribution as defined by Sections 1 through 9 of this document.
 
 "Licensor" shall mean the Rights owner or entity authorized by the Rights owner that is granting the License.
- 
+ 
 "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship or design. For the purposes of this License, Derivative Works shall not include works that remain reversibly separable from, or merely link (or bind by name) or physically connect to or interoperate with the Work and Derivative Works thereof.
 
 "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form or the application of a Source form to physical material, including but not limited to compiled object code, generated documentation, the instantiation of a hardware design or physical object or material and conversions to other media types, including intermediate forms such as bytecodes, FPGA bitstreams, moulds, artwork and semiconductor topographies (mask works).
diff --git a/options/license/SISSL b/options/license/SISSL
index 7d6ad9d66c..af38d02d92 100644
--- a/options/license/SISSL
+++ b/options/license/SISSL
@@ -36,13 +36,13 @@ Sun Industry Standards Source License - Version 1.1
 
 2.0 SOURCE CODE LICENSE
 
-     2.1 The Initial Developer Grant  The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims: 
+     2.1 The Initial Developer Grant  The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims: 
 
           (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer to use, reproduce, modify, display, perform, sublicense and distribute the Original Code (or portions thereof) with or without Modifications, and/or as part of a Larger Work; and
 
           (b) under Patents Claims infringed by the making, using or selling of Original Code, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Code (or portions thereof).
           (c) the licenses granted in this Section 2.1(a) and (b) are effective on the date Initial Developer first distributes Original Code under the terms of this License.
-          (d) Notwithstanding Section 2.1(b) above, no patent license is granted: 1) for code that You delete from the Original Code; 2) separate from the Original Code; or 3) for infringements caused by: i) the modification of the Original Code or ii) the combination of the Original Code with other software or devices, including but not limited to Modifications. 
+          (d) Notwithstanding Section 2.1(b) above, no patent license is granted: 1) for code that You delete from the Original Code; 2) separate from the Original Code; or 3) for infringements caused by: i) the modification of the Original Code or ii) the combination of the Original Code with other software or devices, including but not limited to Modifications. 
 
 3.0 DISTRIBUTION OBLIGATIONS
 
@@ -92,14 +92,14 @@ This License represents the complete agreement concerning subject matter hereof.
 
 EXHIBIT A - Sun Standards License
 
-"The contents of this file are subject to the Sun Standards License Version 1.1 (the "License"); You may not use this file except in compliance with the License. You may obtain a copy of the License at _______________________________.
+"The contents of this file are subject to the Sun Standards License Version 1.1 (the "License"); You may not use this file except in compliance with the License. You may obtain a copy of the License at _______________________________.
 
-Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either 
+Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either 
 express or implied. See the License for the specific language governing rights and limitations under the License.
 
 The Original Code is ______________________________________.
 
-The Initial Developer of the Original Code is: 
+The Initial Developer of the Original Code is: 
 Sun Microsystems, Inc..
 
 Portions created by: _______________________________________
diff --git a/options/license/Sun-PPP b/options/license/Sun-PPP
new file mode 100644
index 0000000000..5f94a13437
--- /dev/null
+++ b/options/license/Sun-PPP
@@ -0,0 +1,13 @@
+Copyright (c) 2001 by Sun Microsystems, Inc.
+All rights reserved.
+
+Non-exclusive rights to redistribute, modify, translate, and use
+this software in source and binary forms, in whole or in part, is
+hereby granted, provided that the above copyright notice is
+duplicated in any source form, and that neither the name of the
+copyright holder nor the author is used to endorse or promote
+products derived from this software.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/options/license/UMich-Merit b/options/license/UMich-Merit
new file mode 100644
index 0000000000..93e304b90e
--- /dev/null
+++ b/options/license/UMich-Merit
@@ -0,0 +1,19 @@
+[C] The Regents of the University of Michigan and Merit Network, Inc. 1992,
+1993, 1994, 1995 All Rights Reserved
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted, provided
+that the above copyright notice and this permission notice appear in all
+copies of the software and derivative works or modified versions thereof,
+and that both the copyright notice and this permission and disclaimer
+notice appear in supporting documentation.
+
+THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE REGENTS OF THE
+UNIVERSITY OF MICHIGAN AND MERIT NETWORK, INC. DO NOT WARRANT THAT THE
+FUNCTIONS CONTAINED IN THE SOFTWARE WILL MEET LICENSEE'S REQUIREMENTS OR
+THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE.  The Regents of the
+University of Michigan and Merit Network, Inc. shall not be liable for any
+special, indirect, incidental or consequential damages with respect to any
+claim by Licensee or any third party arising from use of the software.
diff --git a/options/license/W3C-19980720 b/options/license/W3C-19980720
index a8554039ef..134879044d 100644
--- a/options/license/W3C-19980720
+++ b/options/license/W3C-19980720
@@ -4,7 +4,7 @@ Copyright (c) 1994-2002 World Wide Web Consortium, (Massachusetts Institute of T
 
 This W3C work (including software, documents, or other related items) is being provided by the copyright holders under the following license. By obtaining, using and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions:
 
-Permission to use, copy, modify, and distribute this software and its documentation, with or without modification,  for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the software and documentation or portions thereof, including modifications, that you make:
+Permission to use, copy, modify, and distribute this software and its documentation, with or without modification,  for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the software and documentation or portions thereof, including modifications, that you make:
 
      1.	The full text of this NOTICE in a location viewable to users of the redistributed or derivative work.
 
diff --git a/options/license/bcrypt-Solar-Designer b/options/license/bcrypt-Solar-Designer
new file mode 100644
index 0000000000..8cb05017fc
--- /dev/null
+++ b/options/license/bcrypt-Solar-Designer
@@ -0,0 +1,11 @@
+Written by Solar Designer <solar at openwall.com> in 1998-2014.
+No copyright is claimed, and the software is hereby placed in the public
+domain.  In case this attempt to disclaim copyright and place the software
+in the public domain is deemed null and void, then the software is
+Copyright (c) 1998-2014 Solar Designer and it is hereby released to the
+general public under the following terms:
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted.
+
+There's ABSOLUTELY NO WARRANTY, express or implied.
diff --git a/options/license/gtkbook b/options/license/gtkbook
new file mode 100644
index 0000000000..91215e80d6
--- /dev/null
+++ b/options/license/gtkbook
@@ -0,0 +1,6 @@
+Copyright 2005 Syd Logan, All Rights Reserved
+
+This code is distributed without warranty. You are free to use
+this code for any purpose, however, if this code is republished or
+redistributed in its original form, as hardcopy or electronically,
+then you must include this copyright notice along with the code.
diff --git a/options/license/softSurfer b/options/license/softSurfer
new file mode 100644
index 0000000000..1bbc88c34c
--- /dev/null
+++ b/options/license/softSurfer
@@ -0,0 +1,6 @@
+Copyright 2001, softSurfer (www.softsurfer.com)
+This code may be freely used and modified for any purpose                                                                                                                                
+providing that this copyright notice is included with it.
+SoftSurfer makes no warranty for this code, and cannot be held
+liable for any real or imagined damage resulting from its use.
+Users of this code must verify correctness for their application.
diff --git a/options/license/threeparttable b/options/license/threeparttable
new file mode 100644
index 0000000000..498b728226
--- /dev/null
+++ b/options/license/threeparttable
@@ -0,0 +1,3 @@
+This file may be distributed, modified, and used in other works with just
+one restriction: modified versions must clearly indicate the modification
+(a name change, or a displayed message, or ?).
diff --git a/options/license/xzoom b/options/license/xzoom
new file mode 100644
index 0000000000..f312dedbc2
--- /dev/null
+++ b/options/license/xzoom
@@ -0,0 +1,12 @@
+Copyright Itai Nahshon 1995, 1996.
+This program is distributed with no warranty.
+
+Source files for this program may be distributed freely.
+Modifications to this file are okay as long as:
+ a. This copyright notice and comment are preserved and
+    left at the top of the file.
+ b. The man page is fixed to reflect the change.
+ c. The author of this change adds his name and change
+    description to the list of changes below.
+Executable files may be distributed with sources, or with
+exact location where the source code can be obtained.
diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index e9428ebcd4..82a8fe5d45 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -4,6 +4,7 @@ explore=Procházet
 help=Nápověda
 logo=Logo
 sign_in=Přihlásit se
+sign_in_with_provider=Přihlásit se pomocí %s
 sign_in_or=nebo
 sign_out=Odhlásit se
 sign_up=Registrovat se
@@ -16,6 +17,7 @@ template=Šablona
 language=Jazyk
 notifications=Oznámení
 active_stopwatch=Aktivní sledování času
+tracked_time_summary=Shrnutí sledovaného času na základě filtrů v seznamu úkolů
 create_new=Vytvořit…
 user_profile_and_more=Profily a nastavení…
 signed_in_as=Přihlášen jako
@@ -23,6 +25,7 @@ enable_javascript=Tato stránka vyžaduje JavaScript.
 toc=Obsah
 licenses=Licence
 return_to_gitea=Vrátit se do Gitea
+more_items=Více položek
 
 username=Uživatelské jméno
 email=E-mailová adresa
@@ -73,12 +76,13 @@ collaborative=Spolupráce
 forks=Rozštěpení
 
 activities=Aktivity
-pull_requests=Požadavky na natažení
+pull_requests=Pull requesty
 issues=Úkoly
 milestones=Milníky
 
 ok=OK
 cancel=Zrušit
+retry=Znovu
 rerun=Znovu spustit
 rerun_all=Znovu spustit všechny úlohy
 save=Uložit
@@ -86,14 +90,17 @@ add=Přidat
 add_all=Přidat vše
 remove=Odstranit
 remove_all=Odstranit vše
-remove_label_str=`Odstranit položku "%s"`
+remove_label_str=Odstranit položku „%s“
 edit=Upravit
+view=Zobrazit
 
 enabled=Povolený
 disabled=Zakázané
+locked=Uzamčeno
 
 copy=Kopírovat
 copy_url=Kopírovat URL
+copy_hash=Kopírovat hash
 copy_content=Kopírovat obsah
 copy_branch=Kopírovat jméno větve
 copy_success=Zkopírováno!
@@ -106,6 +113,8 @@ loading=Načítá se…
 
 error=Chyba
 error404=Stránka, kterou se snažíte zobrazit, buď <strong>neexistuje</strong>, nebo <strong>nemáte oprávnění</strong> ji zobrazit.
+go_back=Zpět
+invalid_data=Neplatná data: %v
 
 never=Nikdy
 unknown=Neznámý
@@ -116,6 +125,7 @@ pin=Připnout
 unpin=Odepnout
 
 artifacts=Artefakty
+confirm_delete_artifact=Jste si jisti, že chcete odstranit artefakt „%s“?
 
 archived=Archivováno
 
@@ -127,11 +137,50 @@ concept_user_organization=Organizace
 show_timestamps=Zobrazit časové značky
 show_log_seconds=Zobrazit sekundy
 show_full_screen=Zobrazit celou obrazovku
+download_logs=Stáhnout logy
 
+confirm_delete_selected=Potvrdit odstranění všech vybraných položek?
 
 name=Název
 value=Hodnota
 
+filter=Filtr
+filter.clear=Vymazat filtr
+filter.is_archived=Archivováno
+filter.not_archived=Nearchivované
+filter.is_fork=Rozštěpený
+filter.not_fork=Není rozštěpený
+filter.is_mirror=Zrcadlen
+filter.not_mirror=Není zrcadleno
+filter.is_template=Šablona
+filter.not_template=Není šablona
+filter.public=Veřejná
+filter.private=Soukromý
+
+no_results_found=Nebyly nalezeny žádné výsledky.
+
+[search]
+search=Hledat...
+type_tooltip=Druh vyhledávání
+fuzzy=Fuzzy
+fuzzy_tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu
+match=Shoda
+match_tooltip=Zahrnout pouze výsledky, které odpovídají přesnému hledanému výrazu
+repo_kind=Hledat repozitáře...
+user_kind=Hledat uživatele...
+org_kind=Hledat organizace...
+team_kind=Hledat týmy...
+code_kind=Hledat kód...
+code_search_unavailable=Vyhledávání kódu není momentálně dostupné. Obraťte se na správce webu.
+code_search_by_git_grep=Aktuální výsledky vyhledávání kódu jsou poskytovány pomocí „git grep“. Pokud správce webu povolí index repozitáře, mohou být výsledky lepší.
+package_kind=Hledat balíčky...
+project_kind=Hledat projekty...
+branch_kind=Hledat větve...
+commit_kind=Hledat commity...
+runner_kind=Hledat runnery...
+no_results=Nebyly nalezeny žádné odpovídající výsledky.
+keyword_search_unavailable=Hledání podle klíčového slova není momentálně dostupné. Obraťte se na správce webu.
+
 [aria]
 navbar=Navigační lišta
 footer=Patička
@@ -153,7 +202,7 @@ buttons.code.tooltip=Přidat kód
 buttons.link.tooltip=Přidat odkaz
 buttons.list.unordered.tooltip=Přidat seznam odrážek
 buttons.list.ordered.tooltip=Přidat číslovaný seznam
-buttons.list.task.tooltip=Přidat seznam úkolů
+buttons.list.task.tooltip=Přidat seznam úloh
 buttons.mention.tooltip=Uveďte uživatele nebo tým
 buttons.ref.tooltip=Odkaz na issue nebo pull request
 buttons.switch_to_legacy.tooltip=Místo toho použít starší editor
@@ -166,6 +215,7 @@ string.desc=Z – A
 
 [error]
 occurred=Došlo k chybě
+report_message=Pokud jste si jisti, že se jedná o chybu Gitea, prosím vyhledejte problém na <a href="https://github.com/go-gitea/gitea/issues" target="_blank">GitHub</a> a v případě potřeby založte nový problém.
 missing_csrf=Špatný požadavek: Neexistuje CSRF token
 invalid_csrf=Špatný požadavek: Neplatný CSRF token
 not_found=Cíl nebyl nalezen.
@@ -174,6 +224,7 @@ network_error=Chyba sítě
 [startpage]
 app_desc=Snadno přístupný vlastní Git
 install=Jednoduchá na instalaci
+install_desc=Jednoduše <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">spusťte jako binární program</a> pro vaši platformu, nasaďte jej pomocí <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a>, nebo jej stáhněte jako <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">balíček</a>.
 platform=Multiplatformní
 platform_desc=Gitea běží všude, kde <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> může kompilovat: Windows, macOS, Linux, ARM, atd. Vyberte si ten, který milujete!
 lightweight=Lehká
@@ -218,6 +269,7 @@ repo_path_helper=Všechny vzdálené repozitáře Gitu budou uloženy do tohoto
 lfs_path=Kořenový adresář Git LFS
 lfs_path_helper=V tomto adresáři budou uloženy soubory, které jsou sledovány Git LFS. Pokud ponecháte prázdné, LFS zakážete.
 run_user=Spustit jako uživatel
+run_user_helper=Zadejte uživatelské jméno, pod kterým Gitea běží v operačním systému. Pozor: tento uživatel musí mít přístup ke kořenovému adresáři repozitářů.
 domain=Doména serveru
 domain_helper=Adresa domény, nebo hostitele serveru.
 ssh_port=Port SSH serveru
@@ -234,6 +286,7 @@ email_title=Nastavení e-mailu
 smtp_addr=Server SMTP
 smtp_port=Port SMTP
 smtp_from=Odeslat e-mail jako
+smtp_from_invalid=Adresa "Odeslat e-mail jako" je neplatná
 smtp_from_helper=E-mailová adresa, kterou bude Gitea používat. Zadejte běžnou e-mailovou adresu, nebo použijte formát "Jméno"<email@example.com>.
 mailer_user=Uživatelské jméno SMTP
 mailer_password=Heslo pro SMTP
@@ -267,7 +320,7 @@ install_btn_confirm=Nainstalovat Gitea
 test_git_failed=Chyba při testu příkazu 'git': %v
 sqlite3_not_available=Tato verze Gitea nepodporuje SQLite3. Stáhněte si oficiální binární verzi od %s (nikoli verzi „gobuild“).
 invalid_db_setting=Nastavení databáze je neplatné: %v
-invalid_db_table=Databázová tabulka "%s" je neplatná: %v
+invalid_db_table=Databázová tabulka „%s“ je neplatná: %v
 invalid_repo_path=Kořenový adresář repozitářů není správný: %v
 invalid_app_data_path=Cesta k datům aplikace je neplatná: %v
 run_user_not_match=`"Run as" uživatelské jméno není aktuální uživatelské jméno: %s -> %s`
@@ -289,8 +342,11 @@ invalid_password_algorithm=Neplatný algoritmus hash hesla
 password_algorithm_helper=Nastavte algoritmus hashování hesla. Algoritmy mají odlišné požadavky a sílu. Algoritmus argon2 je poměrně bezpečný, ale používá spoustu paměti a může být nevhodný pro malé systémy.
 enable_update_checker=Povolit kontrolu aktualizací
 enable_update_checker_helper=Kontroluje vydání nových verzí pravidelně připojením ke gitea.io.
+env_config_keys=Konfigurace prostředí
+env_config_keys_prompt=Následující proměnné prostředí budou také použity pro váš konfigurační soubor:
 
 [home]
+nav_menu=Navigační menu
 uname_holder=Uživatelské jméno nebo e-mailová adresa
 password_holder=Heslo
 switch_dashboard_context=Přepnout kontext přehledu
@@ -300,7 +356,6 @@ collaborative_repos=Společné repozitáře
 my_orgs=Mé organizace
 my_mirrors=Má zrcadla
 view_home=Zobrazit %s
-search_repos=Nalézt repozitář…
 filter=Ostatní filtry
 filter_by_team_repositories=Filtrovat podle repozitářů týmu
 feed_of=Kanál z „%s“
@@ -321,20 +376,8 @@ issues.in_your_repos=Ve vašich repozitářích
 repos=Repozitáře
 users=Uživatelé
 organizations=Organizace
-search=Vyhledat
 go_to=Přejít na
 code=Kód
-search.type.tooltip=Druh vyhledávání
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu
-search.match=Shoda
-search.match.tooltip=Zahrnout pouze výsledky, které odpovídají přesnému hledanému výrazu
-code_search_unavailable=V současné době není vyhledávání kódu dostupné. Obraťte se na správce webu.
-repo_no_results=Nebyly nalezeny žádné odpovídající repozitáře.
-user_no_results=Nebyly nalezeni žádní odpovídající uživatelé.
-org_no_results=Nebyly nalezeny žádné odpovídající organizace.
-code_no_results=Nebyl nalezen žádný zdrojový kód odpovídající hledanému výrazu.
-code_search_results=`Výsledky hledání pro "%s"`
 code_last_indexed_at=Naposledy indexováno %s
 relevant_repositories_tooltip=Repozitáře, které jsou rozštěpení nebo nemají žádné téma, ikonu a žádný popis jsou skryty.
 relevant_repositories=Zobrazují se pouze relevantní repositáře, <a href="%s">zobrazit nefiltrované výsledky</a>.
@@ -347,25 +390,31 @@ disable_register_prompt=Registrace jsou vypnuty. Prosíme, kontaktujte správce
 disable_register_mail=E-mailové potvrzení o registraci je zakázané.
 manual_activation_only=Pro dokončení aktivace kontaktujte správce webu.
 remember_me=Pamatovat si toto zařízení
+remember_me.compromised=Přihlašovací token již není platný, což může znamenat napadení účtu. Zkontrolujte prosím svůj účet pro neobvyklé aktivity.
 forgot_password_title=Zapomenuté heslo
 forgot_password=Zapomenuté heslo?
 sign_up_now=Potřebujete účet? Zaregistrujte se.
-confirmation_mail_sent_prompt=Na adresu <b>%s</b> byl zaslán nový potvrzovací e-mail. Zkontrolujte prosím vaši doručenou poštu během následujících %s, abyste dokončili proces registrace.
+sign_up_successful=Účet byl úspěšně vytvořen. Vítejte!
+confirmation_mail_sent_prompt_ex=Nový potvrzovací e-mail byl odeslán na <b>%s</b>. Zkontrolujte prosím svou doručenou poštu během následujících %s a dokončete proces registrace. Pokud je Vaše registrační e-mailová adresa nesprávná, můžete se znovu přihlásit a změnit ji.
 must_change_password=Aktualizujte své heslo
 allow_password_change=Vyžádat od uživatele změnu hesla (doporučeno)
 reset_password_mail_sent_prompt=Na adresu <b>%s</b> byl zaslán potvrzovací e-mail. Zkontrolujte prosím vaši doručenou poštu během následujících %s, abyste dokončili proces obnovení účtu.
 active_your_account=Aktivujte si váš účet
 account_activated=Účet byl aktivován
 prohibit_login=Přihlášení zakázáno
+prohibit_login_desc=Vašemu účtu je zakázáno se přihlásit, kontaktujte prosím správce webu.
 resent_limit_prompt=Omlouváme se, ale před chvílí jste požádal o zaslání aktivačního e-mailu. Počkejte prosím 3 minuty a pak to zkuste znovu.
 has_unconfirmed_mail=Zdravím, %s, máte nepotvrzenou e-mailovou adresu (<b>%s</b>). Pokud jste nedostali e-mail pro potvrzení nebo potřebujete zaslat nový, klikněte prosím na tlačítku níže.
+change_unconfirmed_mail_address=Pokud je Vaše registrační e-mailová adresa nesprávná, můžete ji zde změnit a znovu odeslat nový potvrzovací e-mail.
 resend_mail=Klikněte zde pro odeslání aktivačního e-mailu
 email_not_associate=Tato e-mailová adresa není spojena s žádným účtem.
 send_reset_mail=Zaslat e-mail pro obnovení účtu
 reset_password=Obnovení účtu
 invalid_code=Tento potvrzující kód je neplatný nebo mu vypršela platnost.
+invalid_code_forgot_password=Váš potvrzovací kód je neplatný nebo mu vypršela platnost. <a href="%s">Klikněte zde</a> pro vytvoření nového kódu.
 invalid_password=Vaše heslo se neshoduje s heslem, které bylo použito k vytvoření účtu.
 reset_password_helper=Obnovit účet
+reset_password_wrong_user=Jste přihlášen/a jako %s, ale odkaz pro obnovení účtu je pro %s
 password_too_short=Délka hesla musí být minimálně %d znaků.
 non_local_account=Externě ověřovaní uživatelé nemohou aktualizovat své heslo prostřednictvím webového rozhraní Gitea.
 verify=Ověřit
@@ -390,6 +439,7 @@ openid_connect_title=Připojení k existujícímu účtu
 openid_connect_desc=Zvolené OpenID URI není známé. Přidružte nový účet zde.
 openid_register_title=Vytvořit nový účet
 openid_register_desc=Zvolené OpenID URI není známé. Přidružte nový účet zde.
+openid_signin_desc=Zadejte vaši OpenID URI. Například: alice.openid.example.org nebo https://openid.example.org/alice.
 disable_forgot_password_mail=Obnovení účtu je zakázáno, protože není nastaven žádný e-mail. Obraťte se na správce webu.
 disable_forgot_password_mail_admin=Obnovení účtu je dostupné pouze po nastavení e-mailu. Pro povolení obnovy účtu nastavte prosím e-mail.
 email_domain_blacklisted=Nemůžete se registrovat s vaší e-mailovou adresou.
@@ -399,8 +449,11 @@ authorize_application_created_by=Tuto aplikaci vytvořil %s.
 authorize_application_description=Pokud povolíte přístup, bude moci přistupovat a zapisovat do všech vašich informací o účtu včetně soukromých repozitářů a organizací.
 authorize_title=Autorizovat „%s“ pro přístup k vašemu účtu?
 authorization_failed=Autorizace selhala
+authorization_failed_desc=Autorizace selhala, protože jsme detekovali neplatný požadavek. Kontaktujte prosím správce aplikace, kterou jste se pokoušeli autorizovat.
 sspi_auth_failed=SSPI autentizace selhala
+password_pwned=Heslo, které jste zvolili, je na <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">seznamu odcizených hesel</a>, která byla dříve odhalena při narušení veřejných dat. Zkuste to prosím znovu s jiným heslem.
 password_pwned_err=Nelze dokončit požadavek na HaveIBeenPwned
+last_admin=Nelze odstranit posledního správce. Musí existovat alespoň jeden správce.
 
 [mail]
 view_it_on=Zobrazit na %s
@@ -414,6 +467,7 @@ activate_account.text_1=Ahoj <b>%[1]s</b>, děkujeme za registraci na %[2]s!
 activate_account.text_2=Pro aktivaci vašeho účtu do <b>%s</b> klikněte na následující odkaz:
 
 activate_email=Ověřte vaši e-mailovou adresu
+activate_email.title=%s, prosím ověřte vaši e-mailovou adresu
 activate_email.text=Pro aktivaci vašeho účtu do <b>%s</b> klikněte na následující odkaz:
 
 register_notify=Vítejte v Gitea
@@ -428,7 +482,7 @@ reset_password.text=Klikněte prosím na následující odkaz pro obnovení vaš
 
 register_success=Registrace byla úspěšná
 
-issue_assigned.pull=@%[1]s vás přiřadil/a k požadavku na natažení %[2]s repozitáři %[3]s.
+issue_assigned.pull=@%[1]s vás přiřadil/a k pull requestu %[2]s v repozitáři %[3]s.
 issue_assigned.issue=@%[1]s vás přiřadil/a k úkolu %[2]s repozitáři %[3]s.
 
 issue.x_mentioned_you=<b>@%s</b> vás zmínil/a:
@@ -438,11 +492,11 @@ issue.action.push_n=<b>@%[1]s</b> nahrál/a %[3]d commity do %[2]s
 issue.action.close=<b>@%[1]s</b> uzavřel/a #%[2]d.
 issue.action.reopen=<b>@%[1]s</b> znovu otevřel/a #%[2]d.
 issue.action.merge=<b>@%[1]s</b> sloučil/a #%[2]d do %[3]s.
-issue.action.approve=<b>@%[1]s</b> schválil/a tento požadavek na natažení.
-issue.action.reject=<b>@%[1]s</b> požadoval/a změny v tomto požadavku na natažení.
-issue.action.review=<b>@%[1]s</b> okomentoval/a tento požadavek na natažení.
-issue.action.review_dismissed=<b>@%[1]s</b> odmítl/a poslední kontrolu z %[2]s pro tento požadavek na natažení.
-issue.action.ready_for_review=<b>@%[1]s</b> označil/a tento požadavek na natažení jako připravený ke kontrole.
+issue.action.approve=<b>@%[1]s</b> schválil/a tento pull request.
+issue.action.reject=<b>@%[1]s</b> požadoval/a změny v tomto pull requestu.
+issue.action.review=<b>@%[1]s</b> okomentoval/a tento pull request.
+issue.action.review_dismissed=<b>@%[1]s</b> odmítl/a poslední kontrolu z %[2]s pro tento pull request.
+issue.action.ready_for_review=<b>@%[1]s</b> označil/a tento pull request jako připravený ke kontrole.
 issue.action.new=<b>@%[1]s</b> vytvořil/a #%[2]d.
 issue.in_tree_path=V %s:
 
@@ -509,6 +563,7 @@ url_error=`„%s“ není platná adresa URL.`
 include_error=` musí obsahovat substring „%s“.`
 glob_pattern_error=`zástupný vzor je neplatný: %s.`
 regex_pattern_error=` regex vzor je neplatný: %s.`
+username_error=` může obsahovat pouze alfanumerické znaky („0-9“, „a-z“, „A-Z“), pomlčku („-“), podtržítka („_“) a tečka („.“). Nemůže začínat nebo končit nealfanumerickými znaky a po sobě jdoucí nealfanumerické znaky jsou také zakázány.`
 invalid_group_team_map_error=` mapování je neplatné: %s`
 unknown_error=Neznámá chyba:
 captcha_incorrect=CAPTCHA kód není správný.
@@ -531,6 +586,7 @@ team_name_been_taken=Název týmu je již použit.
 team_no_units_error=Povolit přístup alespoň do jedné sekce repozitáře.
 email_been_used=Tato e-mailová adresa je již používána.
 email_invalid=Emailová adresa je neplatná.
+email_domain_is_not_allowed=Doména uživatelského e-mailu <b>%s</b> je v rozporu s EMAIL_DOMAIN_ALLOWLIST nebo EMAIL_DOMAIN_BLOCKLIST. Ujistěte se, že je Vaše operace očekávána.
 openid_been_used=OpenID addresa „%s“ je již použita.
 username_password_incorrect=Uživatelské jméno nebo heslo není správné.
 password_complexity=Heslo nesplňuje požadavky na složitost:
@@ -542,6 +598,8 @@ enterred_invalid_repo_name=Zadaný název repozitáře není správný.
 enterred_invalid_org_name=Zadaný název organizace není správný.
 enterred_invalid_owner_name=Nové jméno vlastníka není správné.
 enterred_invalid_password=Zadané heslo není správné.
+unset_password=Přihlášený uživatel nenastavil heslo.
+unsupported_login_type=Typ přihlášení není podporován pro odstranění účtu.
 user_not_exist=Tento uživatel neexistuje.
 team_not_exist=Tento tým neexistuje.
 last_org_owner=Nemůžete odstranit posledního uživatele z týmu „vlastníci“. Musí existovat alespoň jeden vlastník pro organizaci.
@@ -553,13 +611,22 @@ invalid_ssh_key=Nelze ověřit váš SSH klíč: %s
 invalid_gpg_key=Nelze ověřit váš GPG klíč: %s
 invalid_ssh_principal=Neplatný SSH Principal certifikát: %s
 must_use_public_key=Zadaný klíč je soukromý klíč. Nenahrávejte svůj soukromý klíč nikde. Místo toho použijte váš veřejný klíč.
+unable_verify_ssh_key=Nelze ověřit váš SSH klíč.
 auth_failed=Ověření selhalo: %v
 
+still_own_repo=Váš účet vlastní jeden nebo více repozitářů. Nejprve je smažte nebo převeďte.
+still_has_org=Váš účet je členem jedné nebo více organizací. Nejdříve je musíte opustit.
+still_own_packages=Váš účet vlastní jeden nebo více balíčků. Nejprve je musíte odstranit.
+org_still_own_repo=Organizace stále vlastní jeden nebo více repozitářů. Nejdříve je smažte nebo převeďte.
+org_still_own_packages=Organizace stále vlastní jeden nebo více balíčků. Nejdříve je smažte.
 
 target_branch_not_exist=Cílová větev neexistuje.
 
+admin_cannot_delete_self=Nemůžete se smazat, dokud jste správce. Nejdříve prosím odeberte svá administrátorská oprávnění.
+
 [user]
 change_avatar=Změnit váš avatar…
+joined_on=Přidal/a se %s
 repositories=Repozitáře
 activity=Veřejná aktivita
 followers=Sledující
@@ -575,10 +642,36 @@ user_bio=Životopis
 disabled_public_activity=Tento uživatel zakázal veřejnou viditelnost aktivity.
 email_visibility.limited=Vaše e-mailová adresa je viditelná pro všechny ověřené uživatele
 email_visibility.private=Vaše e-mailová adresa je viditelná pouze pro vás a administrátory
+show_on_map=Zobrazit toto místo na mapě
+settings=Uživatelská nastavení
 
-form.name_reserved=Uživatelské jméno "%s" je rezervováno.
-form.name_pattern_not_allowed=Vzor "%s" není povolen v uživatelském jméně.
-form.name_chars_not_allowed=Uživatelské jméno "%s" obsahuje neplatné znaky.
+form.name_reserved=Uživatelské jméno „%s“ je rezervováno.
+form.name_pattern_not_allowed=Vzor „%s“ není povolen v uživatelském jméně.
+form.name_chars_not_allowed=Uživatelské jméno „%s“ obsahuje neplatné znaky.
+
+block.block=Blokovat
+block.block.user=Zablokovat Uživatele
+block.block.org=Blokovat uživatele pro organizaci
+block.block.failure=Nepodařilo se zablokovat uživatele: %s
+block.unblock=Odblokovat
+block.unblock.failure=Nepodařilo se odblokovat uživatele: %s
+block.blocked=Zablokovali jste tohoto uživatele.
+block.title=Zablokovat Uživatele
+block.info=Blokování uživatele brání v interakci s repozitáři, jako je otevírání nebo komentování pull requestů nebo úkolů. Další informace o blokování uživatele.
+block.info_1=Zablokování uživatele zabrání následujícím akcím na vašem účtu a repozirářích:
+block.info_2=sledují váš účet
+block.info_3=pošle vám oznámení pomocí @zmínění vašeho uživatelského jména
+block.info_4=pozváním vás jako spolupracovníka do jejich repozitářů
+block.info_5=oblíbení, rozštěpení nebo sledování repozitářů
+block.info_6=otevření a komentování úkolů nebo pull requestů
+block.info_7=reagovat na své komentáře v úkolech nebo pull requestů
+block.user_to_block=Uživatel k blokování
+block.note=Poznámka
+block.note.title=Volitelná poznámka:
+block.note.info=Poznámka není pro blokovaného uživatele viditelná.
+block.note.edit=Upravit poznámku
+block.list=Blokovaní uživatelé
+block.list.none=Nemáte blokované žádné uživatele.
 
 [settings]
 profile=Profil
@@ -596,9 +689,13 @@ delete=Smazat účet
 twofa=Dvoufaktorové ověřování
 account_link=Propojené účty
 organization=Organizace
+uid=UID
 webauthn=Bezpečnostní klíče
 
 public_profile=Veřejný profil
+biography_placeholder=Řekněte nám něco o sobě! (Můžete použít Markdown)
+location_placeholder=Sdílejte svou přibližnou polohu s ostatními
+profile_desc=Nastavte, jak bude váš profil zobrazen ostatním uživatelům. Vaše hlavní e-mailová adresa bude použita pro oznámení, obnovení hesla a operace Git.
 password_username_disabled=Externí uživatelé nemohou měnit svoje uživatelské jméno. Kontaktujte prosím svého administrátora pro více detailů.
 full_name=Celé jméno
 website=Web
@@ -606,15 +703,20 @@ location=Místo
 update_theme=Aktualizovat motiv vzhledu
 update_profile=Aktualizovat profil
 update_language=Aktualizovat jazyk
-update_language_not_found=Jazyk "%s" není k dispozici.
+update_language_not_found=Jazyk „%s“ není k dispozici.
 update_language_success=Jazyk byl aktualizován.
 update_profile_success=Váš profil byl aktualizován.
 change_username=Vaše uživatelské jméno bylo změněno.
+change_username_prompt=Poznámka: Změna uživatelského jména také změní URL vašeho účtu.
+change_username_redirect_prompt=Staré uživatelské jméno bude přesměrováváno, dokud nebude znovu obsazeno.
 continue=Pokračovat
 cancel=Zrušit
 language=Jazyk
 ui=Motiv vzhledu
 hidden_comment_types=Skryté typy komentářů
+hidden_comment_types_description=Zde zkontrolované typy komentářů nebudou zobrazeny na stránkách problémů. Zaškrtnutí „Štítek“ například odstraní všechny komentáře „<user> přidal/odstranil <label>“.
+hidden_comment_types.ref_tooltip=Komentáře, na které se odkazovalo z jiného úkolu/commitu/…
+hidden_comment_types.issue_ref_tooltip=Komentáře, kde uživatel změní větev/značku spojenou s problémem
 comment_type_group_reference=Reference
 comment_type_group_label=Štítek
 comment_type_group_milestone=Milník
@@ -631,6 +733,7 @@ comment_type_group_project=Projekt
 comment_type_group_issue_ref=Referenční číslo úkolu
 saved_successfully=Vaše nastavení bylo úspěšně uloženo.
 privacy=Soukromí
+keep_activity_private=Skrýt aktivitu z profilové stránky
 keep_activity_private_popup=Učinit aktivitu viditelnou pouze pro vás a administrátory
 
 lookup_avatar_by_mail=Vyhledat avatar pomocí e-mailové adresy
@@ -640,12 +743,14 @@ choose_new_avatar=Vybrat nový avatar
 update_avatar=Aktualizovat avatar
 delete_current_avatar=Smazat aktuální avatar
 uploaded_avatar_not_a_image=Nahraný soubor není obrázek.
+uploaded_avatar_is_too_big=Nahraný soubor (%d KiB) přesahuje maximální velikost (%d KiB).
 update_avatar_success=Vaše avatar byl aktualizován.
 update_user_avatar_success=Uživatelův avatar byl aktualizován.
 
 change_password=Aktualizovat heslo
 old_password=Stávající heslo
 new_password=Nové heslo
+retype_new_password=Potvrdit nové heslo
 password_incorrect=Zadané heslo není správné.
 change_password_success=Vaše heslo bylo aktualizováno. Od teď se přihlašujte novým heslem.
 password_change_disabled=Externě ověřovaní uživatelé nemohou aktualizovat své heslo prostřednictvím webového rozhraní Gitea.
@@ -654,6 +759,7 @@ emails=E-mailová adresa
 manage_emails=Správa e-mailových adres
 manage_themes=Vyberte výchozí motiv vzhledu
 manage_openid=Správa OpenID adres
+email_desc=Vaše hlavní e-mailová adresa bude použita pro oznámení, obnovení hesla, a pokud není skrytá, pro operace Gitu.
 theme_desc=Toto bude váš výchozí motiv vzhledu napříč stránkou.
 primary=Hlavní
 activated=Aktivován
@@ -661,6 +767,7 @@ requires_activation=Vyžaduje aktivaci
 primary_email=Nastavit jako hlavní
 activate_email=Odeslat aktivaci
 activations_pending=Čekající aktivace
+can_not_add_email_activations_pending=Existuje čekající aktivace, zkuste to znovu za pár minut, pokud chcete přidat nový e-mail.
 delete_email=Smazat
 email_deletion=Odstranit e-mailovou adresu
 email_deletion_desc=E-mailová adresa a přidružené informace budou z vašeho účtu odstraněny. Commity Gitu s touto e-mailovou adresou zůstanou nezměněny. Pokračovat?
@@ -674,10 +781,12 @@ add_new_email=Přidat novou e-mailovou adresu
 add_new_openid=Přidat novou OpenID URI
 add_email=Přidat e-mailovou adresu
 add_openid=Přidat OpenID URI
+add_email_confirmation_sent=Potvrzovací e-mail byl odeslán na „%s“. Prosím zkontrolujte příchozí poštu během následujících %s pro potvrzení vaší e-mailové adresy.
 add_email_success=Nová e-mailová adresa byla přidána.
 email_preference_set_success=Nastavení e-mailu bylo úspěšně nastaveno.
 add_openid_success=Nová OpenID adresa byla přidána.
 keep_email_private=Schovat e-mailovou adresu
+keep_email_private_popup=Toto skryje vaši e-mailovou adresu z vašeho profilu, stejně jako při vytvoření pull requestu nebo úpravě souboru pomocí webového rozhraní. Odeslané commity nebudou změněny. Použijte %s v commitech pro jejich přiřazení k vašemu účtu.
 openid_desc=OpenID vám umožní delegovat ověřování na externího poskytovatele.
 
 manage_ssh_keys=Správa klíčů SSH
@@ -708,10 +817,9 @@ gpg_invalid_token_signature=Zadaný GPG klíč, podpis a token se neshodují neb
 gpg_token_required=Musíte zadat podpis pro níže uvedený token
 gpg_token=Token
 gpg_token_help=Podpis můžete vygenerovat pomocí:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Zakódovaný podpis GPG
 key_signature_gpg_placeholder=Začíná s „-----BEGIN PGP SIGNATURE-----“
-verify_gpg_key_success=GPG klíč "%s" byl ověřen.
+verify_gpg_key_success=GPG klíč „%s“ byl ověřen.
 ssh_key_verified=Ověřený klíč
 ssh_key_verified_long=Klíč byl ověřen pomocí tokenu a může být použit k ověření commitů shodujících se s libovolnou vaší aktivovanou e-mailovou adresou pro tohoto uživatele.
 ssh_key_verify=Ověřit
@@ -721,14 +829,15 @@ ssh_token=Token
 ssh_token_help=Podpis můžete vygenerovat pomocí:
 ssh_token_signature=Zakódovaný podpis SSH
 key_signature_ssh_placeholder=Začíná s „-----BEGIN SSH SIGNATURE-----“
-verify_ssh_key_success=SSH klíč "%s" byl ověřen.
+verify_ssh_key_success=SSH klíč „%s“ byl ověřen.
 subkeys=Podklíče
 key_id=ID klíče
 key_name=Název klíče
 key_content=Obsah
 principal_content=Obsah
-add_key_success=SSH klíč "%s" byl přidán.
-add_gpg_key_success=GPG klíč "%s" byl přidán.
+add_key_success=SSH klíč „%s“ byl přidán.
+add_gpg_key_success=GPG klíč „%s“ byl přidán.
+add_principal_success=Byl přidán SSH Principal certifikát „%s“.
 delete_key=Odstranit
 ssh_key_deletion=Odstraňte SSH klíč
 gpg_key_deletion=Odstraňte GPG klíč
@@ -755,7 +864,9 @@ ssh_disabled=SSH zakázáno
 ssh_signonly=SSH je v současné době zakázáno, proto jsou tyto klíče použity pouze pro ověření podpisu.
 ssh_externally_managed=Tento SSH klíč je spravován externě pro tohoto uživatele
 manage_social=Správa propojených účtů sociálních sítí
+social_desc=Tyto účty sociálních sítí lze použít k přihlášení k vašemu účtu. Ujistěte se, že jsou všechny vaše.
 unbind=Odpojit
+unbind_success=Účet sociální sítě byl úspěšně odstraněn.
 
 manage_access_token=Spravovat přístupové tokeny
 generate_new_token=Vygenerovat nový token
@@ -776,6 +887,8 @@ permissions_access_all=Vše (veřejné, soukromé a omezené)
 select_permissions=Vyberte oprávnění
 permission_no_access=Bez přístupu
 permission_read=Přečtené
+permission_write=čtení i zápis
+access_token_desc=Vybraná oprávnění tokenu omezují autorizaci pouze na odpovídající trasy <a %s>API</a>. Přečtěte si <a %s>dokumentaci</a> pro více informací.
 at_least_one_permission=Musíte vybrat alespoň jedno oprávnění pro vytvoření tokenu
 permissions_list=Oprávnění:
 
@@ -787,6 +900,8 @@ remove_oauth2_application_desc=Odstraněním OAuth2 aplikace odeberete přístup
 remove_oauth2_application_success=Aplikace byla odstraněna.
 create_oauth2_application=Vytvořit novou OAuth2 aplikaci
 create_oauth2_application_button=Vytvořit aplikaci
+create_oauth2_application_success=Úspěšně jste vytvořili novou OAuth2 aplikaci.
+update_oauth2_application_success=Úspěšně jste aktualizovali OAuth2 aplikaci.
 oauth2_application_name=Název aplikace
 oauth2_confidential_client=Důvěrný klient. Vyberte aplikace, které zachovávají důvěrnosti v utajení, jako jsou webové aplikace. Nevybírejte pro nativní aplikace včetně stolních a mobilních aplikací.
 oauth2_redirect_uris=Přesměrování URI. Použijte nový řádek pro každou URI.
@@ -795,19 +910,26 @@ oauth2_client_id=ID klienta
 oauth2_client_secret=Tajný klíč klienta
 oauth2_regenerate_secret=Obnovit tajný klíč
 oauth2_regenerate_secret_hint=Ztratili jste svůj tajný klíč?
+oauth2_client_secret_hint=Tajný klíč se znovu nezobrazí po opuštění nebo obnovení této stránky. Ujistěte se, že jste si jej uložili.
 oauth2_application_edit=Upravit
 oauth2_application_create_description=OAuth2 aplikace poskytuje přístup aplikacím třetích stran k uživatelským účtům na této instanci.
+oauth2_application_remove_description=Odebráním OAuth2 aplikace zabrání přístupu ověřeným uživatelům na této instanci. Pokračovat?
+oauth2_application_locked=Gitea předregistruje některé OAuth2 aplikace při spuštění, pokud je to povoleno v konfiguraci. Aby se zabránilo neočekávanému chování, nelze je upravovat ani odstranit. Více informací naleznete v dokumentaci OAuth2.
 
 authorized_oauth2_applications=Autorizovat OAuth2 aplikaci
+authorized_oauth2_applications_description=Úspěšně jste povolili přístup k vašemu osobnímu účtu této aplikaci třetí strany. Zrušte prosím přístup aplikacím, které již nadále nepotřebujete.
 revoke_key=Zrušit
 revoke_oauth2_grant=Zrušit přístup
 revoke_oauth2_grant_description=Zrušením přístupu této aplikaci třetí strany ji zabráníte v přístupu k vašim datům. Jste si jisti?
+revoke_oauth2_grant_success=Přístup byl úspěšně zrušen.
 
 twofa_desc=Dvoufaktorový způsob ověřování zvýší zabezpečení vašeho účtu.
+twofa_recovery_tip=Pokud ztratíte své zařízení, budete moci použít jednorázový obnovovací klíč k získání přístupu k vašemu účtu.
 twofa_is_enrolled=Váš účet aktuálně <strong>používá</strong> dvoufaktorové ověřování.
 twofa_not_enrolled=Váš účet aktuálně nepoužívá dvoufaktorové ověřování.
 twofa_disable=Zakázat dvoufaktorové ověřování
 twofa_scratch_token_regenerate=Obnovit pomocný token
+twofa_scratch_token_regenerated=Váš jednorázový obnovovací klíč je nyní %s. Uložte jej na bezpečném místě, protože se znovu nezobrazí.
 twofa_enroll=Povolit dvoufaktorové ověřování
 twofa_disable_note=Dvoufaktorové ověřování můžete zakázat, když bude potřeba.
 twofa_disable_desc=Zakážete-li dvoufaktorové ověřování, bude váš účet méně zabezpečený. Pokračovat?
@@ -825,6 +947,8 @@ webauthn_register_key=Přidat bezpečnostní klíč
 webauthn_nickname=Přezdívka
 webauthn_delete_key=Odstranit bezpečnostní klíč
 webauthn_delete_key_desc=Pokud odstraníte bezpečnostní klíč, již se s ním nebudete moci přihlásit. Pokračovat?
+webauthn_key_loss_warning=Pokud ztratíte své bezpečnostní klíče, ztratíte přístup k vašemu účtu.
+webauthn_alternative_tip=Možná budete chtít nakonfigurovat další metodu ověřování.
 
 manage_account_links=Správa propojených účtů
 manage_account_links_desc=Tyto externí účty jsou propojeny s vaším Gitea účtem.
@@ -834,8 +958,10 @@ remove_account_link=Odstranit propojený účet
 remove_account_link_desc=Odstraněním propojeného účtu zrušíte jeho přístup k vašemu Gitea účtu. Pokračovat?
 remove_account_link_success=Propojený účet byl odstraněn.
 
+hooks.desc=Přidat webhooky, které budou spouštěny pro <strong>všechny repozitáře</strong> vve vašem vlastnictví.
 
 orgs_none=Nejste členem žádné organizace.
+repos_none=Nevlastníte žádné repozitáře.
 
 delete_account=Smazat váš účet
 delete_prompt=Tato operace natrvalo odstraní váš uživatelský účet. <strong>NELZE</strong> ji vrátit zpět.
@@ -854,9 +980,12 @@ visibility=Viditelnost uživatele
 visibility.public=Veřejný
 visibility.public_tooltip=Viditelné pro všechny
 visibility.limited=Omezený
+visibility.limited_tooltip=Viditelné pouze pro ověřené uživatele
 visibility.private=Soukromý
+visibility.private_tooltip=Viditelné pouze pro členy organizací, ke kterým jste se připojili
 
 [repo]
+new_repo_helper=Repozitář obsahuje všechny projektové soubory, včetně historie revizí. Už jej hostujete jinde? <a href="%s">Migrovat repozitář.</a>
 owner=Vlastník
 owner_helper=Některé organizace se nemusejí v seznamu zobrazit kvůli maximálnímu dosaženému počtu repozitářů.
 repo_name=Název repozitáře
@@ -868,6 +997,7 @@ template_helper=Z repozitáře vytvořit šablonu
 template_description=Šablony repozitářů umožňují uživatelům generovat nové repositáře se stejnou strukturou, soubory a volitelnými nastaveními.
 visibility=Viditelnost
 visibility_description=Pouze majitelé nebo členové organizace to budou moci vidět, pokud mají práva.
+visibility_helper=Nastavit repozitář jako soukromý
 visibility_helper_forced=Váš administrátor vynutil, že nové repozitáře budou soukromé.
 visibility_fork_helper=(Změna tohoto ovlivní všechny rozštěpení repozitáře.)
 clone_helper=Potřebujete pomoci s klonováním? Navštivte <a target="_blank" rel="noopener noreferrer" href="%s">nápovědu</a>.
@@ -876,8 +1006,12 @@ fork_from=Rozštěpit z
 already_forked=Již jsi rozštěpil %s
 fork_to_different_account=Rozštěpit na jiný účet
 fork_visibility_helper=Viditelnost rozštěpeného repozitáře nemůže být změněna.
+fork_branch=Větev, která má být klonována pro fork
+all_branches=Všechny větve
+fork_no_valid_owners=Tento repozitář nemůže být rozštěpen, protože neexistují žádní platní vlastníci.
+fork.blocked_user=Nelze rozštěpit repozitář, protože jste blokováni majitelem repozitáře.
 use_template=Použít tuto šablonu
-clone_in_vsc=Klonovat ve VS Code
+open_with_editor=Otevřít pomocí %s
 download_zip=Stáhnout ZIP
 download_tar=Stáhnout TAR.GZ
 download_bundle=Stáhnout BUNDLE
@@ -893,6 +1027,8 @@ issue_labels_helper=Vyberte sadu štítků úkolů.
 license=Licence
 license_helper=Vyberte licenční soubor.
 license_helper_desc=Licence řídí, co ostatní mohou a nemohou dělat s vaším kódem. Nejste si jisti, která je pro váš projekt správná? Podívejte se na <a target="_blank" rel="noopener noreferrer" href="%s">Zvolte licenci</a>
+object_format=Formát objektu
+object_format_helper=Objektový formát repozitáře. Nelze později změnit. SHA1 je nejvíce kompatibilní.
 readme=README
 readme_helper=Vyberte šablonu souboru README.
 readme_helper_desc=Toto je místo, kde můžete napsat úplný popis vašeho projektu.
@@ -904,14 +1040,18 @@ trust_model_helper_collaborator_committer=Spolupracovník+Přispěvatel: Důvě
 trust_model_helper_default=Výchozí: Použít výchozí model důvěry pro tuto instalaci
 create_repo=Vytvořit repozitář
 default_branch=Výchozí větev
-default_branch_helper=Výchozí větev je základní větev pro požadavky na natažení a commity kódu.
+default_branch_label=výchozí
+default_branch_helper=Výchozí větev je základní větev pro pull requesty a commity kódu.
 mirror_prune=Vyčistit
 mirror_prune_desc=Odstranit zastaralé reference na vzdálené sledování
 mirror_interval=Interval zrcadlení (platné časové jednotky jsou „h“, „m“ a „s“). 0 zakáže periodickou synchronizaci. (Minimální interval: %s)
 mirror_interval_invalid=Interval zrcadlení není platný.
+mirror_sync=synchronizováno
 mirror_sync_on_commit=Synchronizovat při nahrávání revizí
 mirror_address=Klonovat z URL
 mirror_address_desc=Zadejte požadované přístupové údaje do sekce Ověření.
+mirror_address_url_invalid=Poskytnutá URL je neplatná. Všechny části musíte správně nahradit escape sekvencí.
+mirror_address_protocol_invalid=Zadaná URL je neplatná. Mohou být zrcadleny pouze umístění http(s):// nebo git://.
 mirror_lfs=Úložiště velkých souborů (LFS)
 mirror_lfs_desc=Aktivovat zrcadlení dat LFS.
 mirror_lfs_endpoint=Koncový bod LFS
@@ -937,19 +1077,27 @@ delete_preexisting=Odstranit již existující soubory
 delete_preexisting_content=Odstranit soubory v %s
 delete_preexisting_success=Smazány nepřijaté soubory v %s
 blame_prior=Zobrazit blame před touto změnou
+blame.ignore_revs=Ignorování revizí v <a href="%s">.git-blame-ignorerevs</a>. Klikněte zde <a href="%s">pro obejití</a> a zobrazení normálního pohledu blame.
+blame.ignore_revs.failed=Nepodařilo se ignorovat revize v <a href="%s">.git-blame-ignore-revs</a>.
 author_search_tooltip=Zobrazí maximálně 30 uživatelů
 
+tree_path_not_found_commit=Cesta %[1]s v commitu %[2]s neexistuje
+tree_path_not_found_branch=Cesta %[1]s ve větvi %[2]s neexistuje
+tree_path_not_found_tag=Cesta %[1]s ve značce %[2]s neexistuje
 
 transfer.accept=Přijmout převod
 transfer.accept_desc=Převést do „%s“
 transfer.reject=Odmítnout převod
 transfer.reject_desc=Zrušit převod do „%s“
+transfer.no_permission_to_accept=Nemáte oprávnění k přijetí tohoto převodu.
+transfer.no_permission_to_reject=Nemáte oprávnění k odmítnutí tohoto převodu.
 
 desc.private=Soukromý
 desc.public=Veřejný
 desc.template=Šablona
 desc.internal=Interní
 desc.archived=Archivováno
+desc.sha256=SHA256
 
 template.items=Položky šablony
 template.git_content=Obsah gitu (výchozí větev)
@@ -962,12 +1110,15 @@ template.issue_labels=Štítky úkolů
 template.one_item=Musíte vybrat alespoň jednu položku šablony
 template.invalid=Musíte vybrat repositář šablony
 
+archive.title=Tento repozitář je archivovaný. Můžete prohlížet soubory, klonovat, ale nemůžete nahrávat a vytvářet nové úkoly nebo pull requesty.
+archive.title_date=Tento repositář byl archivován %s. Můžete zobrazit soubory a klonovat je, ale nemůžete nahrávat ani otevírat problémy nebo pull requesty.
 archive.issue.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat úkoly.
-archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat požadavky na natažení.
+archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat pull requesty.
 
 form.reach_limit_of_creation_1=Již jste dosáhli svůj limit %d repozitář.
 form.reach_limit_of_creation_n=Již jste dosáhli svůj limit %d repozitářů.
 form.name_reserved=Název repozitáře „%s“ je rezervován.
+form.name_pattern_not_allowed=Vzor „%s“ není povolený v názvu repozitáře.
 
 need_auth=Ověření
 migrate_options=Možnosti migrace
@@ -977,12 +1128,13 @@ migrate_options_lfs=Migrovat LFS soubory
 migrate_options_lfs_endpoint.label=Koncový bod LFS
 migrate_options_lfs_endpoint.description=Migrace se pokusí použít váš vzdálený Git pro <a target="_blank" rel="noopener noreferrer" href="%s">určení LFS serveru</a>. Můžete také zadat vlastní koncový bod, pokud jsou data LFS repozitáře uložena někde jinde.
 migrate_options_lfs_endpoint.description.local=Podporována je také cesta k lokálnímu serveru.
+migrate_options_lfs_endpoint.placeholder=Ponecháte-li prázdné, koncový bod bude odvozen z adresy URL klonu
 migrate_items=Položky pro migrování
 migrate_items_wiki=Wiki
 migrate_items_milestones=Milníky
 migrate_items_labels=Štítky
 migrate_items_issues=Úkoly
-migrate_items_pullrequests=Požadavky na natažení
+migrate_items_pullrequests=Pull requesty
 migrate_items_merge_requests=Sloučit požadavky
 migrate_items_releases=Vydání
 migrate_repo=Migrovat repozitář
@@ -992,6 +1144,7 @@ migrate.github_token_desc=Můžete sem vložit jeden nebo více tokenů oddělen
 migrate.clone_local_path=nebo místní cesta serveru
 migrate.permission_denied=Není dovoleno importovat místní repozitáře.
 migrate.permission_denied_blocked=Nelze importovat z nepovolených hostitelů, prosím požádejte správce, aby zkontroloval nastavení ALLOWED_DOMAINS/ALLOW_LOCALETWORKS/BLOCKED_DOMAINS.
+migrate.invalid_local_path=Místní cesta je neplatná, buď neexistuje nebo není adresářem.
 migrate.invalid_lfs_endpoint=Koncový bod LFS není platný.
 migrate.failed=Přenesení selhalo: %v
 migrate.migrate_items_options=Pro migraci dalších položek je vyžadován přístupový token
@@ -1016,7 +1169,7 @@ migrate.migrating_milestones=Migrování milnků
 migrate.migrating_labels=Migrování štítků
 migrate.migrating_releases=Migrování vydání
 migrate.migrating_issues=Migrování úkolů
-migrate.migrating_pulls=Migrování požadavků na natažení
+migrate.migrating_pulls=Migrování pull requestů
 migrate.cancel_migrating_title=Zrušit migraci
 migrate.cancel_migrating_confirm=Chcete zrušit tuto migraci?
 
@@ -1032,6 +1185,7 @@ watch=Sledovat
 unstar=Odoblíbit
 star=Oblíbit
 fork=Rozštěpit
+action.blocked_user=Nelze provést akci, protože jste zablokování vlastníkem repozitáře.
 download_archive=Stáhnout repozitář
 more_operations=Další operace
 
@@ -1054,7 +1208,7 @@ find_tag=Najít značku
 branches=Větve
 tags=Značky
 issues=Úkoly
-pulls=Požadavky na natažení
+pulls=Pull requesty
 project_board=Projekty
 packages=Balíčky
 actions=Akce
@@ -1069,6 +1223,7 @@ release=Vydání
 releases=Vydání
 tag=Značka
 released_this=vydal/a toto
+tagged_this=označil/a
 file.title=%s v %s
 file_raw=Surový
 file_history=Historie
@@ -1077,6 +1232,10 @@ file_view_rendered=Zobrazit vykreslené
 file_view_raw=Zobrazit v surovém stavu
 file_permalink=Trvalý odkaz
 file_too_large=Soubor je příliš velký pro zobrazení.
+invisible_runes_header=`Tento soubor obsahuje neviditelné znaky Unicode`
+invisible_runes_description=`Tento soubor obsahuje neviditelné Unicode znaky, které jsou pro člověka nerozeznatelné, ale mohou být zpracovány jiným způsobem. Pokud si myslíte, že je to záměrné, můžete toto varování bezpečně ignorovat. Použijte tlačítko Escape sekvence k jejich zobrazení.`
+ambiguous_runes_header=`Tento soubor obsahuje nejednoznačné znaky Unicode`
+ambiguous_runes_description=`Tento soubor obsahuje znaky Unicode, které mohou být zaměněny s jinými znaky. Pokud si myslíte, že je to záměrné, můžete toto varování bezpečně ignorovat. Použijte tlačítko Escape sekvence k jejich zobrazení.`
 invisible_runes_line=`Tento řádek má neviditelné znaky Unicode`
 ambiguous_runes_line=`Tento řádek má nejednoznačné znaky Unicode`
 ambiguous_character=`%[1]c [U+%04[1]X] je zaměnitelný s %[2]c [U+%04[2]X]`
@@ -1089,11 +1248,17 @@ video_not_supported_in_browser=Váš prohlížeč nepodporuje značku pro HTML5
 audio_not_supported_in_browser=Váš prohlížeč nepodporuje značku pro HTML5 audio.
 stored_lfs=Uloženo pomocí Git LFS
 symbolic_link=Symbolický odkaz
+executable_file=Spustitelný soubor
+vendored=Vendorováno
+generated=Generováno
 commit_graph=Graf commitů
 commit_graph.select=Vybrat větve
-commit_graph.hide_pr_refs=Skrýt požadavky na natažení
+commit_graph.hide_pr_refs=Skrýt pull requesty
 commit_graph.monochrome=Černobílé
 commit_graph.color=Barva
+commit.contained_in=Tento commit je obsažen v:
+commit.contained_in_default_branch=Tento commit je součástí výchozí větve
+commit.load_referencing_branches_and_tags=Načíst větve a značky odkazující na tento commit
 blame=Blame
 download_file=Stáhnout soubor
 normal_view=Normální zobrazení
@@ -1127,11 +1292,12 @@ editor.update=Aktualizovat %s
 editor.delete=Odstranit %s
 editor.patch=Použít záplatu
 editor.patching=Záplatování:
+editor.fail_to_apply_patch=Nelze použít záplatu „%s“
 editor.new_patch=Nová záplata
 editor.commit_message_desc=Přidat volitelný rozšířený popis…
 editor.signoff_desc=Přidat Signed-off-by podpis přispěvatele na konec zprávy o commitu.
 editor.commit_directly_to_this_branch=Odevzdat přímo do větve <strong class="branch-name">%s</strong>.
-editor.create_new_branch=Vytvořit <strong>novou větev</strong> pro tento commit a spustit požadavek na natažení.
+editor.create_new_branch=Vytvořit <strong>novou větev</strong> pro tento commit a začít pull request.
 editor.create_new_branch_np=Vytvořte <strong>novou větev</strong> z tohoto commitu.
 editor.propose_file_change=Navrhnout změnu souboru
 editor.new_branch_name=Pojmenujte novou větev pro tento commit
@@ -1141,7 +1307,14 @@ editor.filename_cannot_be_empty=Jméno nemůže být prázdné.
 editor.filename_is_invalid=Název souboru je neplatný: „%s“.
 editor.branch_does_not_exist=Větev „%s“ v tomto repozitáři neexistuje.
 editor.branch_already_exists=Větev „%s“ již existuje v tomto repozitáři.
+editor.directory_is_a_file=Jméno adresáře „%s“ je již použito jako jméno souboru v tomto repozitáři.
+editor.file_is_a_symlink=`„%s“ je symbolický odkaz. Symbolické odkazy nemohou být upravovány ve webovém editoru`
+editor.filename_is_a_directory=Jméno souboru „%s“ je již použito jako jméno adresáře v tomto repozitáři.
+editor.file_editing_no_longer_exists=Upravovaný soubor „%s“ již není součástí tohoto repozitáře.
+editor.file_deleting_no_longer_exists=Odstraňovaný soubor „%s“ již není součástí tohoto repozitáře.
 editor.file_changed_while_editing=Obsah souboru byl změněn od doby, kdy jste začaly s úpravou. <a target="_blank" rel="noopener noreferrer" href="%s">Klikněte zde</a>, abyste je zobrazili, nebo <strong>potvrďte změny ještě jednou</strong> pro jejich přepsání.
+editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři.
+editor.commit_id_not_matching=ID commitu se neshoduje s ID, když jsi začal/a s úpravami. Odevzdat do záplatové větve a poté sloučit.
 editor.commit_empty_file_header=Odevzdat prázdný soubor
 editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. Pokračovat?
 editor.no_changes_to_show=Žádné změny k zobrazení.
@@ -1163,9 +1336,10 @@ editor.revert=Vrátit %s na:
 
 commits.desc=Procházet historii změn zdrojového kódu.
 commits.commits=Commity
+commits.no_commits=Žádné společné commity. „%s“ a „%s“ mají zcela odlišnou historii.
 commits.nothing_to_compare=Tyto větve jsou stejné.
-commits.search=Hledání commitů…
-commits.find=Vyhledat
+commits.search.tooltip=Můžete předřadit klíčová slova s „author:“, „committer:“, „after:“ nebo „before:“, např. „revert author:Alice before:2019-01-03“.
+commits.search_branch=Tato větev
 commits.search_all=Všechny větve
 commits.author=Autor
 commits.message=Zpráva
@@ -1177,6 +1351,7 @@ commits.signed_by_untrusted_user=Podepsáno nedůvěryhodným uživatelem
 commits.signed_by_untrusted_user_unmatched=Podepsáno nedůvěryhodným uživatelem, který nesouhlasí s přispěvatelem
 commits.gpg_key_id=ID GPG klíče
 commits.ssh_key_fingerprint=Otisk klíče SSH
+commits.view_path=Zobrazit v tomto bodě v historii
 
 commit.operations=Operace
 commit.revert=Vrátit
@@ -1202,18 +1377,19 @@ projects.create=Vytvořit projekt
 projects.title=Název
 projects.new=Nový projekt
 projects.new_subheader=Koordinujte, sledujte a aktualizujte svou práci na jednom místě, aby projekty zůstaly transparentní a v plánu.
+projects.create_success=Projekt „%s“ byl vytvořen.
 projects.deletion=Odstranit projekt
 projects.deletion_desc=Odstranění projektu jej odstraní ze všech souvisejících úkolů. Pokračovat?
 projects.deletion_success=Projekt byl odstraněn.
 projects.edit=Upravit projekty
 projects.edit_subheader=Projekty organizují úkoly a sledují pokrok.
 projects.modify=Aktualizovat projekt
+projects.edit_success=Projekt „%s“ byl aktualizován.
 projects.type.none=Žádný
 projects.type.basic_kanban=Základní Kanban
 projects.type.bug_triage=Třídění chyb
 projects.template.desc=Šablona projektu
 projects.template.desc_helper=Vyberte šablonu projektu pro začátek
-projects.type.uncategorized=Nezařazené
 projects.column.edit=Upravit sloupec
 projects.column.edit_title=Název
 projects.column.new_title=Název
@@ -1221,10 +1397,7 @@ projects.column.new_submit=Vytvořit sloupec
 projects.column.new=Nový sloupec
 projects.column.set_default=Nastavit jako výchozí
 projects.column.set_default_desc=Nastavit tento sloupec jako výchozí pro nekategorizované úkoly a požadavky na natažení
-projects.column.unset_default=Zrušit nastavení jako výchozí
-projects.column.unset_default_desc=Zrušit nastavení tohoto sloupce jako výchozí
 projects.column.delete=Smazat sloupec
-projects.column.deletion_desc=Smazání projektového sloupce přesune všechny související problémy do kategorie „Nezařazené“. Pokračovat?
 projects.column.color=Barva
 projects.open=Otevřít
 projects.close=Zavřít
@@ -1233,7 +1406,7 @@ projects.card_type.desc=Náhledy karet
 projects.card_type.images_and_text=Obrázky a text
 projects.card_type.text_only=Pouze text
 
-issues.desc=Organizování hlášení chyb, úkolů a milníků.
+issues.desc=Organizování hlášení chyb, úloh a milníků.
 issues.filter_assignees=Filtrovat zpracovatele
 issues.filter_milestones=Filtrovat milník
 issues.filter_projects=Filtrovat projekt
@@ -1259,12 +1432,15 @@ issues.new.assignees=Zpracovatelé
 issues.new.clear_assignees=Smazat zpracovatele
 issues.new.no_assignees=Bez zpracovatelů
 issues.new.no_reviewers=Žádní posuzovatelé
+issues.new.blocked_user=Nemůžete vytvořit úkol, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře.
+issues.edit.blocked_user=Nemůžete upravovat obsah, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře.
 issues.choose.get_started=Začínáme
 issues.choose.open_external_link=Otevřít
 issues.choose.blank=Výchozí
 issues.choose.blank_about=Vytvořit úkol z výchozí šablony.
 issues.choose.ignore_invalid_templates=Neplatné šablony byly ignorovány
 issues.choose.invalid_templates=%v nalezených neplatných šablon
+issues.choose.invalid_config=Nastavení problému obsahuje chyby:
 issues.no_ref=Není určena žádná větev/značka
 issues.create=Vytvořit úkol
 issues.new_label=Nový štítek
@@ -1301,6 +1477,7 @@ issues.delete_branch_at=`odstranil/a větev <b>%s</b> %s`
 issues.filter_label=Štítek
 issues.filter_label_exclude=`Chcete-li vyloučit štítky, použijte <code>alt</code> + <code>click/enter</code>`
 issues.filter_label_no_select=Všechny štítky
+issues.filter_label_select_no_label=Bez štítku
 issues.filter_milestone=Milník
 issues.filter_milestone_all=Všechny milníky
 issues.filter_milestone_none=Žádné milníky
@@ -1354,6 +1531,7 @@ issues.next=Další
 issues.open_title=otevřený
 issues.closed_title=zavřený
 issues.draft_title=Koncept
+issues.num_comments_1=%d komentář
 issues.num_comments=%d komentářů
 issues.commented_at=`okomentoval <a href="#%s">%s</a>`
 issues.delete_comment_confirm=Jste si jist, že chcete smazat tento komentář?
@@ -1362,25 +1540,37 @@ issues.context.quote_reply=Citovat odpověď
 issues.context.reference_issue=Odkázat v novém úkolu
 issues.context.edit=Upravit
 issues.context.delete=Smazat
+issues.no_content=K dispozici není žádný popis.
 issues.close=Zavřít problém
+issues.comment_pull_merged_at=sloučený commit %[1]s do %[2]s %[3]s
 issues.comment_manually_pull_merged_at=ručně sloučený commit %[1]s do %[2]s %[3]s
 issues.close_comment_issue=Okomentovat a zavřít
 issues.reopen_issue=Znovuotevřít
 issues.reopen_comment_issue=Okomentovat a znovuotevřít
 issues.create_comment=Okomentovat
+issues.comment.blocked_user=Nemůžete vytvořit nebo upravovat komentář, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře.
 issues.closed_at=`uzavřel/a tento úkol <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`znovuotevřel/a tento úkol <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.commit_ref_at=`odkázal na tento úkol z commitu <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_issue_from=`<a href="%[3]s">odkazoval/a na tento úkol %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
-issues.ref_pull_from=`<a href="%[3]s">odkazoval/a na tento požadavek na natažení %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
-issues.ref_closing_from=`<a href="%[3]s">odkazoval/a na požadavek na natažení %[4]s, který uzavře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
-issues.ref_reopening_from=`<a href="%[3]s">odkazoval/a na požadavek na natažení %[4]s, který znovu otevře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+issues.ref_pull_from=`<a href="%[3]s">odkazoval/a na tento pull request %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+issues.ref_closing_from=`<a href="%[3]s">odkazoval/a na pull request %[4]s, který uzavře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+issues.ref_reopening_from=`<a href="%[3]s">odkazoval/a na pull request %[4]s, který znovu otevře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_closed_from=`<a href="%[3]s">uzavřel/a tento úkol %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_reopened_from=`<a href="%[3]s">znovu otevřel/a tento úkol %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_from=`z %[1]s`
 issues.author=Autor
+issues.author_helper=Tento uživatel je autor.
 issues.role.owner=Vlastník
+issues.role.owner_helper=Tento uživatel je vlastníkem tohoto repozitáře.
 issues.role.member=Člen
+issues.role.member_helper=Tento uživatel je členem organizace vlastnící tento repositář.
+issues.role.collaborator=Spolupracovník
+issues.role.collaborator_helper=Tento uživatel byl pozván ke spolupráci v repozitáři.
+issues.role.first_time_contributor=Přispěvatel, který přispívá poprvé
+issues.role.first_time_contributor_helper=Toto je první příspěvek tohoto uživatele do repozitáře.
+issues.role.contributor=Přispěvatel
+issues.role.contributor_helper=Tento uživatel již dříve přispíval do repozitáře.
 issues.re_request_review=Znovu požádat o posouzení
 issues.is_stale=Od tohoto posouzení došlo ke změnám v tomto požadavku na natažení
 issues.remove_request_review=Odstranit žádost o posouzení
@@ -1395,6 +1585,11 @@ issues.label_title=Název štítku
 issues.label_description=Popis štítku
 issues.label_color=Barva štítku
 issues.label_exclusive=Exkluzivní
+issues.label_archive=Archivovat štítek
+issues.label_archived_filter=Zobrazit archivované popisky
+issues.label_archive_tooltip=Archivované štítky jsou ve výchozím nastavení vyloučeny z návrhů při hledání podle popisku.
+issues.label_exclusive_desc=Pojmenujte štítek <code>rozsah/položka</code>, aby se stal vzájemně exkluzivním s jinými štítky <code>rozsah/</code>.
+issues.label_exclusive_warning=Jakékoliv protichůdné rozsahy štítků budou odstraněny při úpravě štítků u úkolů nebo u pull requestů.
 issues.label_count=%d štítků
 issues.label_open_issues=%d otevřených úkolů
 issues.label_edit=Upravit
@@ -1447,6 +1642,7 @@ issues.tracking_already_started=`Již jste spustili sledování času na <a href
 issues.stop_tracking=Zastavit časovač
 issues.stop_tracking_history=`ukončil/a práci %s`
 issues.cancel_tracking=Zahodit
+issues.cancel_tracking_history=`zrušil/a sledování času %s`
 issues.add_time=Přidat čas ručně
 issues.del_time=Odstranit tento časový záznam
 issues.add_time_short=Přidat čas
@@ -1470,6 +1666,7 @@ issues.due_date_form=rrrr-mm-dd
 issues.due_date_form_add=Přidat termín dokončení
 issues.due_date_form_edit=Upravit
 issues.due_date_form_remove=Odstranit
+issues.due_date_not_writer=Potřebujete přístup k zápisu do tohoto repozitáře, abyste mohli aktualizovat datum dokončení problému.
 issues.due_date_not_set=Žádný termín dokončení.
 issues.due_date_added=přidal/a termín dokončení %s %s
 issues.due_date_modified=upravil/a termín termínu z %[2]s na %[1]s %[3]s
@@ -1488,26 +1685,27 @@ issues.dependency.remove=Odstranit
 issues.dependency.remove_info=Odstranit tuto závislost
 issues.dependency.added_dependency=`přidal/a novou závislost %s`
 issues.dependency.removed_dependency=`odstranil/a závislost %s`
-issues.dependency.pr_closing_blockedby=Uzavření tohoto požadavku na natažení je blokováno následujícími úkoly
+issues.dependency.pr_closing_blockedby=Uzavření tohoto pull requestu je blokováno následujícími úkoly
 issues.dependency.issue_closing_blockedby=Uzavření tohoto úkolu je blokováno následujícími úkoly
 issues.dependency.issue_close_blocks=Tento úkol blokuje uzavření následujících úkolů
-issues.dependency.pr_close_blocks=Tento požadavek na natažení blokuje uzavření následujících úkolů
+issues.dependency.pr_close_blocks=Tento pull request blokuje uzavření následujících úkolů
 issues.dependency.issue_close_blocked=Musíte zavřít všechny úkoly, které blokují tento úkol, aby jej bylo možné zavřít.
-issues.dependency.pr_close_blocked=Musíte zavřít všechny úkoly, které blokují tento požadavek na natažení, aby jej bylo možné sloučit.
+issues.dependency.issue_batch_close_blocked=Nelze uzavřít úkoly, které jste vybrali, protože úkol #%d má stále otevřené závislosti
+issues.dependency.pr_close_blocked=Musíte zavřít všechny úkoly, které blokují tento pull request, aby jej bylo možné sloučit.
 issues.dependency.blocks_short=Blokuje
 issues.dependency.blocked_by_short=Závisí na
 issues.dependency.remove_header=Odstranit závislost
 issues.dependency.issue_remove_text=Tímto krokem odeberete závislost z úkolu. Pokračovat?
-issues.dependency.pr_remove_text=Tímto krokem odeberete závislost z požadavku na natažení. Pokračovat?
-issues.dependency.setting=Povolit závislosti pro úkoly a požadavky na natažení
+issues.dependency.pr_remove_text=Tímto krokem odeberete závislost z pull requestu. Pokračovat?
+issues.dependency.setting=Povolit závislosti pro úkoly a pull requesty
 issues.dependency.add_error_same_issue=Úkol nemůže záviset sám na sobě.
 issues.dependency.add_error_dep_issue_not_exist=Související úkol neexistuje.
 issues.dependency.add_error_dep_not_exist=Závislost neexistuje.
 issues.dependency.add_error_dep_exists=Závislost již existuje.
 issues.dependency.add_error_cannot_create_circular=Nemůžete vytvořit závislost dvou úkolů, které se vzájemně blokují.
 issues.dependency.add_error_dep_not_same_repo=Oba úkoly musí být ve stejném repozitáři.
-issues.review.self.approval=Nemůžete schválit svůj požadavek na natažení.
-issues.review.self.rejection=Nemůžete požadovat změny ve svém vlastním požadavku na natažení.
+issues.review.self.approval=Nemůžete schválit svůj pull request.
+issues.review.self.rejection=Nemůžete požadovat změny ve svém vlastním pull requestu.
 issues.review.approve=schválil tyto změny %s
 issues.review.comment=posoudil %s
 issues.review.dismissed=zamítl/a posouzení od %s %s
@@ -1524,6 +1722,9 @@ issues.review.pending.tooltip=Tento komentář není momentálně viditelný pro
 issues.review.review=Posouzení
 issues.review.reviewers=Posuzovatelé
 issues.review.outdated=Zastaralé
+issues.review.outdated_description=Obsah se změnil od chvíle, kdy byl tento komentář vytvořen
+issues.review.option.show_outdated_comments=Zobrazit zastaralé komentáře
+issues.review.option.hide_outdated_comments=Skrýt zastaralé komentáře
 issues.review.show_outdated=Zobrazit zastaralé
 issues.review.hide_outdated=Skrýt zastaralé
 issues.review.show_resolved=Zobrazit vyřešené
@@ -1544,10 +1745,11 @@ issues.reference_link=Reference: %s
 compare.compare_base=základní
 compare.compare_head=porovnat
 
-pulls.desc=Povolit požadavky na natažení a posuzování kódu.
-pulls.new=Nový požadavek na natažení
-pulls.view=Zobrazit požadavek na natažení
-pulls.compare_changes=Nový požadavek na natažení
+pulls.desc=Povolit pull requesty a posuzování kódu.
+pulls.new=Nový pull request
+pulls.new.blocked_user=Nemůžete vytvořit pull request, protože jste zablokování vlastníkem repozitáře.
+pulls.view=Zobrazit pull request
+pulls.compare_changes=Nový pull request
 pulls.allow_edits_from_maintainers=Povolit úpravy od správců
 pulls.allow_edits_from_maintainers_desc=Uživatelé s přístupem k zápisu do základní větve mohou také nahrávat do této větve
 pulls.allow_edits_from_maintainers_err=Aktualizace se nezdařila
@@ -1562,38 +1764,53 @@ pulls.compare_compare=natáhnout z
 pulls.switch_comparison_type=Přepnout typ porovnání
 pulls.switch_head_and_base=Prohodit hlavní a základní větev
 pulls.filter_branch=Filtrovat větev
-pulls.no_results=Nebyly nalezeny žádné výsledky.
-pulls.nothing_to_compare=Tyto větve jsou stejné. Není potřeba vytvářet požadavek na natažení.
-pulls.nothing_to_compare_and_allow_empty_pr=Tyto větve jsou stejné. Tento požadavek na natažení bude prázdný.
-pulls.has_pull_request=`Požadavek na natažení mezi těmito větvemi již existuje: <a href="%[1]s">%[2]s#%[3]d</a>`
-pulls.create=Vytvořit požadavek na natažení
+pulls.show_all_commits=Zobrazit všechny commity
+pulls.show_changes_since_your_last_review=Zobrazit změny od vašeho posledního posouzení
+pulls.showing_only_single_commit=Zobrazuji pouze změny commitu %[1]s
+pulls.showing_specified_commit_range=Zobrazují se pouze změny mezi %[1]s..%[2]s
+pulls.select_commit_hold_shift_for_range=Vyberte commit. Podržte klávesu shift + klepněte pro výběr rozsahu
+pulls.review_only_possible_for_full_diff=Posouzení je možné pouze při zobrazení plného rozlišení
+pulls.filter_changes_by_commit=Filtrovat podle commitu
+pulls.nothing_to_compare=Tyto větve jsou stejné. Není potřeba vytvářet pull request.
+pulls.nothing_to_compare_have_tag=Vybraná větev/značka je stejná.
+pulls.nothing_to_compare_and_allow_empty_pr=Tyto větve jsou stejné. Tento pull request bude prázdný.
+pulls.has_pull_request=`Pull request mezi těmito větvemi již existuje: <a href="%[1]s">%[2]s#%[3]d</a>`
+pulls.create=Vytvořit pull request
 pulls.title_desc=chce sloučit %[1]d commity z větve <code>%[2]s</code> do <code id="branch_target">%[3]s</code>
 pulls.merged_title_desc=sloučil %[1]d commity z větve <code>%[2]s</code> do větve <code>%[3]s</code> před %[4]s
 pulls.change_target_branch_at=`změnil/a cílovou větev z <b>%s</b> na <b>%s</b> %s`
 pulls.tab_conversation=Konverzace
 pulls.tab_commits=Commity
 pulls.tab_files=Změněné soubory
-pulls.reopen_to_merge=Prosíme, otevřete znovu tento požadavek na natažení, aby se provedlo sloučení.
-pulls.cant_reopen_deleted_branch=Tento požadavek na natažení nemůže být znovu otevřen protože větev byla smazána.
+pulls.reopen_to_merge=Prosíme, otevřete znovu tento pull request, aby se provedlo sloučení.
+pulls.cant_reopen_deleted_branch=Tento pull request nemůže být znovu otevřen protože větev byla smazána.
 pulls.merged=Sloučený
+pulls.merged_success=Pull request byl úspěšně sloučen a uzavřen
+pulls.closed=Pull request uzavřen
 pulls.manually_merged=Sloučeno ručně
 pulls.merged_info_text=Větev %s může být nyní odstraněna.
-pulls.is_closed=Požadavek na natažení byl uzavřen.
-pulls.title_wip_desc=`<a href="#">Začněte název s <strong>%s</strong></a> a zamezíte tak nechtěnému sloučení požadavku na natažení.`
-pulls.cannot_merge_work_in_progress=Tento požadavek na natažení je označen jako probíhající práce.
+pulls.is_closed=Pull request byl uzavřen.
+pulls.title_wip_desc=`<a href="#">Začněte název s <strong>%s</strong></a> a zamezíte tak nechtěnému sloučení pull requestu.`
+pulls.cannot_merge_work_in_progress=Tento pull request je označen jako probíhající práce.
 pulls.still_in_progress=Stále probíhá?
 pulls.add_prefix=Přidat prefix <strong>%s</strong>
 pulls.remove_prefix=Odstranit prefix <strong>%s</strong>
-pulls.data_broken=Tento požadavek na natažení je rozbitý kvůli chybějícím informacím o rozštěpení.
-pulls.files_conflicted=Tento požadavek na natažení obsahuje změny, které kolidují s cílovou větví.
+pulls.data_broken=Tento pull request je rozbitý kvůli chybějícím informacím o rozštěpení.
+pulls.files_conflicted=Tento pull request obsahuje změny, které kolidují s cílovou větví.
 pulls.is_checking=Právě probíhá kontrola konfliktů při sloučení. Zkuste to za chvíli.
 pulls.is_ancestor=Tato větev je již součástí cílové větve. Není co sloučit.
 pulls.is_empty=Změny na této větvi jsou již na cílové větvi. Toto bude prázdný commit.
 pulls.required_status_check_failed=Některé požadované kontroly nebyly úspěšné.
 pulls.required_status_check_missing=Některé požadované kontroly chybí.
-pulls.required_status_check_administrator=Jako administrátor stále můžete sloučit tento požadavek na natažení.
-pulls.can_auto_merge_desc=Tento požadavek na natažení může být automaticky sloučen.
-pulls.cannot_auto_merge_desc=Tento požadavek na natažení nemůže být automaticky sloučen, neboť se v něm nachází konflikty.
+pulls.required_status_check_administrator=Jako administrátor stále můžete sloučit tento pull request.
+pulls.blocked_by_approvals=Tento pull request ještě nemá dostatek schválení. Uděleno %d z %d schválení.
+pulls.blocked_by_rejection=Tento pull request obsahuje změny požadované oficiálním posuzovatelem.
+pulls.blocked_by_official_review_requests=Tento pull request obsahuje oficiální žádosti o posouzení.
+pulls.blocked_by_outdated_branch=Tento pull request je zablokován, protože je zastaralý.
+pulls.blocked_by_changed_protected_files_1=Tento pull request je zablokován, protože mění chráněný soubor:
+pulls.blocked_by_changed_protected_files_n=Tento pull request je zablokován, protože mění chráněné soubory:
+pulls.can_auto_merge_desc=Tento pull request může být automaticky sloučen.
+pulls.cannot_auto_merge_desc=Tento pull request nemůže být automaticky sloučen, neboť se v něm nachází konflikty.
 pulls.cannot_auto_merge_helper=Pro vyřešení konfliktů proveďte ruční sloučení.
 pulls.num_conflicting_files_1=%d konfliktní soubor
 pulls.num_conflicting_files_n=%d konfliktních souborů
@@ -1605,20 +1822,21 @@ pulls.waiting_count_1=%d čekající posouzení
 pulls.waiting_count_n=%d čekající posouzení
 pulls.wrong_commit_id=ID commitu musí být ID commitu v cílové větvi
 
-pulls.no_merge_desc=Tento požadavek na natažení nemůže být sloučen, protože všechny možnosti repozitáře na sloučení jsou zakázány.
-pulls.no_merge_helper=Povolte možnosti sloučení v nastavení repozitáře nebo proveďte sloučení požadavku na natažení ručně.
-pulls.no_merge_wip=Požadavek na natažení nemůže být sloučen protože je označen jako nedokončený.
-pulls.no_merge_not_ready=Tento požadavek na natažení není připraven na sloučení, zkontrolujte stav posouzení a kontrolu stavu.
-pulls.no_merge_access=Nemáte oprávnění sloučit tento požadavek na natažení.
+pulls.no_merge_desc=Tento pull request nemůže být sloučen, protože všechny možnosti repozitáře na sloučení jsou zakázány.
+pulls.no_merge_helper=Povolte možnosti sloučení v nastavení repozitáře nebo proveďte sloučení pull requestu ručně.
+pulls.no_merge_wip=Pull request nemůže být sloučen protože je označen jako nedokončený.
+pulls.no_merge_not_ready=Tento pull request není připraven na sloučení, zkontrolujte stav posouzení a kontrolu stavu.
+pulls.no_merge_access=Nemáte oprávnění sloučit tento pull request.
 pulls.merge_pull_request=Vytvořit slučovací commit
 pulls.rebase_merge_pull_request=Rebase pak fast-forward
 pulls.rebase_merge_commit_pull_request=Rebase a poté vytvořit slučovací commit
 pulls.squash_merge_pull_request=Vytvořit squash commit
+pulls.fast_forward_only_merge_pull_request=Pouze fast-forward
 pulls.merge_manually=Sloučeno ručně
 pulls.merge_commit_id=ID slučovacího commitu
 pulls.require_signed_wont_sign=Větev vyžaduje podepsané commity, ale toto sloučení nebude podepsáno
 
-pulls.invalid_merge_option=Nemůžete použít tuto možnost sloučení pro tento požadavek na natažení.
+pulls.invalid_merge_option=Nemůžete použít tuto možnost sloučení pro tento pull request.
 pulls.merge_conflict=Sloučení selhalo: Došlo ke konfliktu při sloučení. Tip: Zkuste jinou strategii
 pulls.merge_conflict_summary=Chybové hlášení
 pulls.rebase_conflict=Sloučení selhalo: Došlo ke konfliktu při rebase commitu: %[1]s. Tip: Zkuste jinou strategii
@@ -1626,10 +1844,11 @@ pulls.rebase_conflict_summary=Chybové hlášení
 pulls.unrelated_histories=Sloučení selhalo: Hlavní a základní revize nesdílí společnou historii. Tip: Zkuste jinou strategii
 pulls.merge_out_of_date=Sloučení selhalo: Základ byl aktualizován při generování sloučení. Tip: Zkuste to znovu.
 pulls.head_out_of_date=Sloučení selhalo: Hlavní revize byla aktualizován při generování sloučení. Tip: Zkuste to znovu.
+pulls.has_merged=Chyba: Pull request byl sloučen, nelze znovu sloučit nebo změnit cílovou větev.
 pulls.push_rejected=Sloučení selhalo: Nahrání bylo zamítnuto. Zkontrolujte háčky Gitu pro tento repozitář.
 pulls.push_rejected_summary=Úplná zpráva o odmítnutí
 pulls.push_rejected_no_message=Sloučení se nezdařilo: Nahrání bylo odmítnuto, ale nebyla nalezena žádná vzdálená zpráva.<br>Zkontrolujte háčky gitu pro tento repozitář
-pulls.open_unmerged_pull_exists=`Nemůžete provést operaci znovuotevření protože je tu čekající požadavek na natažení (#%d) s identickými vlastnostmi.`
+pulls.open_unmerged_pull_exists=`Nemůžete provést operaci znovuotevření protože je tu čekající pull request (#%d) s identickými vlastnostmi.`
 pulls.status_checking=Některé kontroly jsou nedořešeny
 pulls.status_checks_success=Všechny kontroly byly úspěšné
 pulls.status_checks_warning=Některé kontroly nahlásily varování
@@ -1637,36 +1856,49 @@ pulls.status_checks_failure=Některé kontroly se nezdařily
 pulls.status_checks_error=Některé kontroly nahlásily chyby
 pulls.status_checks_requested=Požadováno
 pulls.status_checks_details=Podrobnosti
+pulls.status_checks_hide_all=Skrýt všechny kontroly
+pulls.status_checks_show_all=Zobrazit všechny kontroly
 pulls.update_branch=Aktualizovat větev sloučením
 pulls.update_branch_rebase=Aktualizovat větev pomocí rebase
 pulls.update_branch_success=Aktualizace větve byla úspěšná
 pulls.update_not_allowed=Nemáte oprávnění aktualizovat větev
 pulls.outdated_with_base_branch=Tato větev je zastaralá oproti základní větvi
-pulls.closed_at=`uzavřel/a tento požadavek na natažení <a id="%[1]s" href="#%[1]s">%[2]s</a>`
-pulls.reopened_at=`znovuotevřel/a tento požadavek na natažení <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.close=Zavřít pull request
+pulls.closed_at=`uzavřel/a tento pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.reopened_at=`znovuotevřel/a tento pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.cmd_instruction_hint=`Zobrazit <a class="show-instruction">instrukce příkazové řádky</a>.`
+pulls.cmd_instruction_checkout_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny.
+pulls.cmd_instruction_merge_title=Sloučit
+pulls.cmd_instruction_merge_desc=Slučte změny a aktualizujte je na Gitea.
+pulls.clear_merge_message=Vymazat zprávu o sloučení
+pulls.clear_merge_message_hint=Vymazání zprávy o sloučení odstraní pouze obsah zprávy a ponechá generované přídavky gitu jako "Co-AuthoreBy …".
 
 pulls.auto_merge_button_when_succeed=(Když kontroly uspějí)
 pulls.auto_merge_when_succeed=Automaticky sloučit, když všechny kontroly uspějí
-pulls.auto_merge_newly_scheduled=Požadavek na natažení byl naplánován na sloučení, jakmile všechny kontroly uspějí.
-pulls.auto_merge_has_pending_schedule=%[1]s naplánoval/a tento požadavek na natažení pro automatické sloučení, když všechny kontroly uspějí v %[2]s.
+pulls.auto_merge_newly_scheduled=Pull request byl naplánován na sloučení, jakmile všechny kontroly uspějí.
+pulls.auto_merge_has_pending_schedule=%[1]s naplánoval/a tento pull request pro automatické sloučení, když všechny kontroly uspějí v %[2]s.
 
 pulls.auto_merge_cancel_schedule=Zrušit automatické sloučení
-pulls.auto_merge_not_scheduled=Tento požadavek na natažení není naplánován na automatické sloučení.
-pulls.auto_merge_canceled_schedule=Automatické sloučení bylo zrušeno pro tento požadavek na natažení.
+pulls.auto_merge_not_scheduled=Tento pull request není naplánován na automatické sloučení.
+pulls.auto_merge_canceled_schedule=Automatické sloučení bylo zrušeno pro tento pull request.
 
-pulls.auto_merge_newly_scheduled_comment=`požadavek na automatické sloučení tohoto požadavku na natažení je naplánován, když všechny kontroly uspějí %[1]s`
-pulls.auto_merge_canceled_schedule_comment=`zrušil/a automatické sloučení tohoto požadavku na natažení, když všechny kontroly uspějí %[1]s`
+pulls.auto_merge_newly_scheduled_comment=`požadavek na automatické sloučení tohoto pull requestu je naplánován, když všechny kontroly uspějí %[1]s`
+pulls.auto_merge_canceled_schedule_comment=`zrušil/a automatické sloučení tohoto pull requestu, když všechny kontroly uspějí %[1]s`
 
-pulls.delete.title=Odstranit tento požadavek na natažení?
-pulls.delete.text=Opravdu chcete tento požadavek na natažení smazat? (Tím se trvale odstraní veškerý obsah. Pokud jej hodláte archivovat, zvažte raději jeho uzavření.)
+pulls.delete.title=Odstranit tento pull request?
+pulls.delete.text=Opravdu chcete tento pull request smazat? (Tím se trvale odstraní veškerý obsah. Pokud jej hodláte archivovat, zvažte raději jeho uzavření.)
 
+pulls.recently_pushed_new_branches=Nahráli jste větev <strong>%[1]s</strong> %[2]s
 
+pull.deleted_branch=(odstraněno):%s
 
 milestones.new=Nový milník
 milestones.closed=Zavřen dne %s
+milestones.update_ago=Aktualizováno %s
 milestones.no_due_date=Bez lhůty dokončení
 milestones.open=Otevřít
 milestones.close=Zavřít
+milestones.new_subheader=Milníky vám pomohou organizovat úkoly a sledovat jejich pokrok.
 milestones.completeness=%d%% Dokončeno
 milestones.create=Vytvořit milník
 milestones.title=Název
@@ -1674,18 +1906,35 @@ milestones.desc=Popis
 milestones.due_date=Termín (volitelný)
 milestones.clear=Zrušit
 milestones.invalid_due_date_format=Termín dokončení musí být ve formátu 'rrrr-mm-dd'.
+milestones.create_success=Milník „%s“ byl vytvořen.
 milestones.edit=Upravit milník
 milestones.edit_subheader=Milník organizuje úkoly a sledují pokrok.
 milestones.cancel=Zrušit
 milestones.modify=Aktualizovat milník
+milestones.edit_success=Milník „%s“ byl aktualizován.
 milestones.deletion=Smazat milník
 milestones.deletion_desc=Odstranění milníku jej smaže ze všech souvisejících úkolů. Pokračovat?
 milestones.deletion_success=Milník byl odstraněn.
+milestones.filter_sort.earliest_due_data=Nejstarší datum dokončení
+milestones.filter_sort.latest_due_date=Nejnovější datum dokončení
 milestones.filter_sort.least_complete=Nejméně dokončené
 milestones.filter_sort.most_complete=Nejvíce dokončené
 milestones.filter_sort.most_issues=Nejvíce úkolů
 milestones.filter_sort.least_issues=Nejméně úkolů
 
+signing.will_sign=Tento commit bude podepsána klíčem „%s“.
+signing.wont_sign.error=Došlo k chybě při kontrole, zda může být commit podepsán.
+signing.wont_sign.nokey=K podpisu tohoto commitu není k dispozici žádný klíč.
+signing.wont_sign.never=Commity nejsou nikdy podepsány.
+signing.wont_sign.always=Commity jsou vždy podepsány.
+signing.wont_sign.pubkey=Commit nebude podepsán, protože nemáte veřejný klíč spojený s vaším účtem.
+signing.wont_sign.twofa=Pro podepsání commitů musíte mít povoleno dvoufaktorové ověření.
+signing.wont_sign.parentsigned=Commit nebude podepsán, protože nadřazený commit není podepsán.
+signing.wont_sign.basesigned=Sloučení nebude podepsáno, protože základní commit není podepsaný.
+signing.wont_sign.headsigned=Sloučení nebude podepsáno, protože hlavní revize není podepsána.
+signing.wont_sign.commitssigned=Sloučení nebude podepsáno, protože všechny přidružené revize nejsou podepsány.
+signing.wont_sign.approved=Sloučení nebude podepsáno, protože pull request není schválen.
+signing.wont_sign.not_signed_in=Nejste přihlášeni.
 
 ext_wiki=Přístup k externí Wiki
 ext_wiki.desc=Odkaz do externí Wiki.
@@ -1709,12 +1958,18 @@ wiki.file_revision=Revize stránky
 wiki.wiki_page_revisions=Revize Wiki stránky
 wiki.back_to_wiki=Zpět na wiki stránku
 wiki.delete_page_button=Smazat stránku
+wiki.delete_page_notice_1=Odstranění Wiki stránky „%s“ nemůže být vráceno zpět. Pokračovat?
 wiki.page_already_exists=Stránka Wiki se stejným názvem již existuje.
+wiki.reserved_page=Jméno Wiki stránky „%s“ je rezervováno.
 wiki.pages=Stránky
 wiki.last_updated=Naposledy aktualizováno: %s
 wiki.page_name_desc=Zadejte název této Wiki stránky. Některé speciální názvy jsou: „Home“, „_Sidebar“ a „_Footer“.
+wiki.original_git_entry_tooltip=Zobrazit originální Git soubor namísto použití přátelského odkazu.
 
 activity=Aktivita
+activity.navbar.code_frequency=Frekvence kódu
+activity.navbar.contributors=Přispěvatelé
+activity.navbar.recent_commits=Nedávné commity
 activity.period.filter_label=Období:
 activity.period.daily=1 den
 activity.period.halfweekly=3 dny
@@ -1724,16 +1979,16 @@ activity.period.quarterly=3 měsíce
 activity.period.semiyearly=6 měsíců
 activity.period.yearly=1 rok
 activity.overview=Přehled
-activity.active_prs_count_1=<strong>%d</strong> aktivní požadavek na natažení
-activity.active_prs_count_n=<strong>%d</strong> aktivní požadavky na natažení
-activity.merged_prs_count_1=Sloučený požadavek na natažení
-activity.merged_prs_count_n=Sloučené požadavky na natažení
-activity.opened_prs_count_1=Navrhovaný požadavek na natažení
-activity.opened_prs_count_n=Navrhované požadavky na natažení
+activity.active_prs_count_1=<strong>%d</strong> aktivní pull request
+activity.active_prs_count_n=<strong>%d</strong> aktivních pull requestů
+activity.merged_prs_count_1=Sloučený pull request
+activity.merged_prs_count_n=Sloučené pull requesty
+activity.opened_prs_count_1=Navrhovaný pull request
+activity.opened_prs_count_n=Navrhované pull requesty
 activity.title.user_1=%d uživatel
 activity.title.user_n=%d uživatelů
-activity.title.prs_1=%d Požadavek na natažení
-activity.title.prs_n=%d Požadavků na natažení
+activity.title.prs_1=%d pull request
+activity.title.prs_n=%d pull requestů
 activity.title.prs_merged_by=%s sloučil %s
 activity.title.prs_opened_by=%s navrhl %s
 activity.merged_prs_label=Sloučený
@@ -1752,7 +2007,7 @@ activity.new_issues_count_n=Nové úkoly
 activity.new_issue_label=Otevřený
 activity.title.unresolved_conv_1=%d nevyřešená konverzace
 activity.title.unresolved_conv_n=%d nevyřešených konverzací
-activity.unresolved_conv_desc=Tyto nedávno změněné úkolu a požadavky na natažení ještě nebyly vyřešeny.
+activity.unresolved_conv_desc=Tyto nedávno změněné úkolu a pull requestu ještě nebyly vyřešeny.
 activity.unresolved_conv_label=Otevřít
 activity.title.releases_1=%d Vydání
 activity.title.releases_n=%d Vydání
@@ -1780,16 +2035,10 @@ activity.git_stats_and_deletions=a
 activity.git_stats_deletion_1=%d odebrání
 activity.git_stats_deletion_n=%d odebrání
 
-search=Vyhledat
-search.search_repo=Hledat repozitář
-search.type.tooltip=Druh vyhledávání
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu
-search.match=Shoda
-search.match.tooltip=Zahrnout pouze výsledky, které odpovídají přesnému hledanému výrazu
-search.results=Výsledky hledání „%s“ v <a href="%s">%s</a>
-search.code_no_results=Nebyl nalezen žádný zdrojový kód odpovídající hledanému výrazu.
-search.code_search_unavailable=V současné době není vyhledávání kódu dostupné. Obraťte se na správce webu.
+contributors.contribution_type.filter_label=Typ příspěvku:
+contributors.contribution_type.commits=Commity
+contributors.contribution_type.additions=Přidání
+contributors.contribution_type.deletions=Odstranění
 
 settings=Nastavení
 settings.desc=Nastavení je místo, kde můžete měnit nastavení repozitáře
@@ -1804,7 +2053,16 @@ settings.hooks=Webové háčky
 settings.githooks=Háčky Gitu
 settings.basic_settings=Základní nastavení
 settings.mirror_settings=Nastavení zrcadla
+settings.mirror_settings.docs=Nastavte repozitář pro automatickou synchronizaci commitů, značek a větví s jiným repozitářem.
+settings.mirror_settings.docs.disabled_pull_mirror.instructions=Nastavte váš projekt pro automatické nahrávání commitů, značek a větví do jiného repozitáře. Správce webu zakázal zrcadla pro natažení.
+settings.mirror_settings.docs.disabled_push_mirror.instructions=Nastavte svůj projekt pro automatické natažení commitů, značek a větví z jiného repozitáře.
+settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=Právě teď to lze provést pouze v menu "Nová migrace". Pro více informací prosím konzultujte:
+settings.mirror_settings.docs.disabled_push_mirror.info=Push zrcadla byla zakázána administrátorem vašeho webu.
+settings.mirror_settings.docs.no_new_mirrors=Váš repozitář zrcadlí změny do nebo z jiného repozitáře. Mějte prosím na paměti, že v tuto chvíli nemůžete vytvořit žádná nová zrcadla.
+settings.mirror_settings.docs.can_still_use=I když nemůžete upravit stávající zrcadla nebo vytvořit nová, stále můžete použít své stávající zrcadlo.
+settings.mirror_settings.docs.more_information_if_disabled=Více informací o zrcadlech pro nahrání a natažení naleznete zde:
 settings.mirror_settings.docs.doc_link_title=Jak mohu zrcadlit repozitáře?
+settings.mirror_settings.docs.doc_link_pull_section=sekci "stahovat ze vzdáleného úložiště" v dokumentaci.
 settings.mirror_settings.docs.pulling_remote_title=Stažení ze vzdáleného úložiště
 settings.mirror_settings.mirrored_repository=Zrcadlený repozitář
 settings.mirror_settings.direction=Směr
@@ -1814,8 +2072,11 @@ settings.mirror_settings.last_update=Poslední aktualizace
 settings.mirror_settings.push_mirror.none=Nenastavena žádná zrcadla pro nahrání
 settings.mirror_settings.push_mirror.remote_url=URL vzdáleného Git repozitáře
 settings.mirror_settings.push_mirror.add=Přidat zrcadlo pro nahrání
+settings.mirror_settings.push_mirror.edit_sync_time=Upravit interval synchronizace zrcadla
 
 settings.sync_mirror=Synchronizovat nyní
+settings.pull_mirror_sync_in_progress=V tuto chvíli probíhá nahrávání změn ze vzdáleného %s.
+settings.push_mirror_sync_in_progress=Probíhá nahrávání změn do vzdáleného %s.
 settings.site=Webová stránka
 settings.update_settings=Aktualizovat nastavení
 settings.update_mirror_settings=Aktualizovat nastavení zrcadla
@@ -1825,6 +2086,8 @@ settings.branches.add_new_rule=Přidat nové pravidlo
 settings.advanced_settings=Pokročilá nastavení
 settings.wiki_desc=Povolit Wiki repozitáře
 settings.use_internal_wiki=Používat vestavěnou Wiki
+settings.default_wiki_branch_name=Výchozí název větve Wiki
+settings.failed_to_change_default_wiki_branch=Změna výchozí větve wiki se nezdařila.
 settings.use_external_wiki=Používat externí Wiki
 settings.external_wiki_url=URL externí Wiki
 settings.external_wiki_url_error=URL externí wiki platné URL.
@@ -1846,13 +2109,20 @@ settings.tracker_issue_style.regexp_pattern_desc=První zachycená skupina bude
 settings.tracker_url_format_desc=Použijte zástupné symboly <code>{user}</code>, <code>{repo}</code> a <code>{index}</code> pro uživatelské jméno, jméno repozitáře a číslo úkolu.
 settings.enable_timetracker=Povolit sledování času
 settings.allow_only_contributors_to_track_time=Povolit sledování času pouze přispěvatelům
-settings.pulls_desc=Povolit požadavky na natažení
+settings.pulls_desc=Povolit pull requesty repozitáře
 settings.pulls.ignore_whitespace=Ignorovat bílé znaky při konfliktech
 settings.pulls.enable_autodetect_manual_merge=Povolit autodetekci ručních sloučení (Poznámka: V některých zvláštních případech může dojít k nesprávnému rozhodnutí)
-settings.pulls.allow_rebase_update=Povolit aktualizaci větve požadavku na natažení pomocí rebase
-settings.pulls.default_delete_branch_after_merge=Ve výchozím nastavení mazat větev požadavku na natažení po jeho sloučení
+settings.pulls.allow_rebase_update=Povolit aktualizaci větve pull requestu pomocí rebase
+settings.pulls.default_delete_branch_after_merge=Ve výchozím nastavení mazat větev pull requestu po jeho sloučení
+settings.pulls.default_allow_edits_from_maintainers=Ve výchozím nastavení povolit úpravy od správců
+settings.releases_desc=Povolit vydání v repozitáři
 settings.packages_desc=Povolit registr balíčků repozitáře
 settings.projects_desc=Povolit projekty v repozitáři
+settings.projects_mode_desc=Režim projektů (druhy projektů k zobrazení)
+settings.projects_mode_repo=Pouze projekty repozitáře
+settings.projects_mode_owner=Pouze projekty uživatele nebo organizace
+settings.projects_mode_all=Všechny projekty
+settings.actions_desc=Povolit akce repozitáře
 settings.admin_settings=Nastavení správce
 settings.admin_enable_health_check=Povolit kontrolu stavu repozitáře (git fsck)
 settings.admin_code_indexer=Indexování kódu
@@ -1877,8 +2147,10 @@ settings.convert_fork_succeed=Rozštěpení bylo překonvertován na běžný re
 settings.transfer=Předat vlastnictví
 settings.transfer.rejected=Převod repozitáře byl zamítnut.
 settings.transfer.success=Převod repozitáře byl úspěšný.
+settings.transfer.blocked_user=Nelze převést repozitář, protože jste blokování novým vlastníkem.
 settings.transfer_abort=Zrušit převod
 settings.transfer_abort_invalid=Nemůžete zrušit neexistující převod repozitáře.
+settings.transfer_abort_success=Převod repozitáře do %s byl úspěšně zrušen.
 settings.transfer_desc=Předat tento repozitář uživateli nebo organizaci, ve které máte administrátorská práva.
 settings.transfer_form_title=Zadejte jméno repozitáře pro potvrzení:
 settings.transfer_in_progress=V současné době probíhá převod. Zrušte jej, pokud chcete převést tento repozitář jinému uživateli.
@@ -1898,6 +2170,7 @@ settings.trust_model.collaborator.long=Spolupracovník: Důvěřovat podpisům s
 settings.trust_model.collaborator.desc=Platné podpisy spolupracovníků tohoto repozitáře budou označeny jako „důvěryhodné“ - (ať se shodují s autorem, či nikoli). V opačném případě budou platné podpisy označeny jako „nedůvěryhodné“, pokud se podpis shoduje s přispěvatelem a „neodpovídající“, pokud ne.
 settings.trust_model.committer=Přispěvatel
 settings.trust_model.committer.long=Přispěvatel: Důvěřovat podpisům, které odpovídají autorům (což odpovídá GitHub a přinutí Giteu nastavit jako tvůrce pro Giteou podepsané revize)
+settings.trust_model.committer.desc=Platné podpisy budou označeny pouze jako „důvěryhodné“, pokud se shodují s přispěvatelem, jinak budou označeny jako „neodpovídající“. To přinutí Giteu, aby byla přispěvatelem podepsaných commitů se skutečným přispěvatelem označeným jako Co-authored-by: a Co-committed-by: na konci commitu. Výchozí klíč Gitea musí odpovídat uživateli v databázi.
 settings.trust_model.collaboratorcommitter=Spolupracovník+Přispěvatel
 settings.trust_model.collaboratorcommitter.long=Spolupracovník+Přispěvatel: Důvěřovat podpisům od spolupracovníků, které odpovídají tvůrci revize
 settings.trust_model.collaboratorcommitter.desc=Platné podpisy spolupracovníků tohoto repozitáře budou označeny jako „důvěryhodné“, pokud se shodují s přispěvatelem. V opačném případě budou platné podpisy označeny jako "nedůvěryhodné", pokud se podpis shoduje s přispěvatelem a „neodpovídajícím“ v opačném případě. To přinutí Giteu, aby byla označena jako přispěvatel podepsaných commitů se skutečným přispěvatelem označeným jako Co-Authored-By: a Co-Committed-By: na konci commitu. Výchozí klíč Gitea musí odpovídat uživateli v databázi.
@@ -1913,17 +2186,18 @@ settings.delete_notices_2=- Tato operace trvale smaže repozitář <strong>%s</s
 settings.delete_notices_fork_1=- Rozštěpení repozitáře bude nezávislé po smazání.
 settings.deletion_success=Repozitář byl odstraněn.
 settings.update_settings_success=Nastavení repozitáře bylo aktualizováno.
+settings.update_settings_no_unit=Repozitář by měl povolit alespoň určitý druh interakce.
 settings.confirm_delete=Smazat repozitář
 settings.add_collaborator=Přidat spolupracovníka
 settings.add_collaborator_success=Spolupracovník byl přidán.
 settings.add_collaborator_inactive_user=Nelze přidat neaktivního uživatele jako spolupracovníka.
 settings.add_collaborator_owner=Vlastníka nelze přidat jako spolupracovníka.
 settings.add_collaborator_duplicate=Spolupracovník je již přidán k tomuto repozitáři.
+settings.add_collaborator.blocked_user=Spolupracovník je zablokován vlastníkem repozitáře nebo naopak.
 settings.delete_collaborator=Odstranit
 settings.collaborator_deletion=Odstranit spolupracovníka
 settings.collaborator_deletion_desc=Odstranění spolupracovníka zruší jeho přístup do tohoto repozitáře. Pokračovat?
 settings.remove_collaborator_success=Spolupracovník byl smazán.
-settings.search_user_placeholder=Hledat uživatele…
 settings.org_not_allowed_to_be_collaborator=Organizace nemůže být přidána jako spolupracovník.
 settings.change_team_access_not_allowed=Změna přístupu týmu k repozitáře se omezuje na vlastníka organizace
 settings.team_not_in_organization=Tým není ve stejné organizaci jako repozitář
@@ -1931,7 +2205,6 @@ settings.teams=Týmy
 settings.add_team=Přidat tým
 settings.add_team_duplicate=Tým již má repozitář
 settings.add_team_success=Tým má nyní přístup k repozitáři.
-settings.search_team=Vyhledat tým…
 settings.change_team_permission_tip=Oprávnění týmu je nastaveno na stránce nastavení týmu a nelze je změnit pro každý repozitář
 settings.delete_team_tip=Tento tým má přístup ke všem repositářům a nemůže být odstraněn
 settings.remove_team_success=Přístup týmu k repozitáři byl odstraněn.
@@ -1943,12 +2216,14 @@ settings.webhook_deletion_desc=Odstranění webového háčku smaže jeho nastav
 settings.webhook_deletion_success=Webový háček byl smazán.
 settings.webhook.test_delivery=Test doručitelnosti
 settings.webhook.test_delivery_desc=Vyzkoušet tento webový háček pomocí falešné události.
+settings.webhook.test_delivery_desc_disabled=Chcete-li tento webový háček otestovat s falešnou událostí, aktivujte ho.
 settings.webhook.request=Požadavek
 settings.webhook.response=Odpověď
 settings.webhook.headers=Hlavičky
 settings.webhook.payload=Obsah
 settings.webhook.body=Tělo zprávy
 settings.webhook.replay.description=Zopakovat tento webový háček.
+settings.webhook.replay.description_disabled=Chcete-li znovu spustit tento webový háček, aktivujte jej.
 settings.webhook.delivery.success=Událost byla přidána do fronty doručení. Může to trvat několik sekund, než se zobrazí v historii doručení.
 settings.githooks_desc=Jelikož háčky Gitu jsou spravovány Gitem samotným, můžete upravit soubory háčků k provádění uživatelských operací.
 settings.githook_edit_desc=Je-li háček neaktivní, bude zobrazen vzorový obsah. Nebude-li zadán žádný obsah, háček bude vypnut.
@@ -1995,21 +2270,25 @@ settings.event_issue_milestone=Úkolu přidán milník
 settings.event_issue_milestone_desc=Úkolu přidán nebo odebrán milník.
 settings.event_issue_comment=Komentář k úkolu
 settings.event_issue_comment_desc=Komentář úkolu přidán, upraven nebo smazán.
-settings.event_header_pull_request=Události požadavku na natažení
-settings.event_pull_request=Požadavek na stažení
-settings.event_pull_request_desc=Požadavek na natažení otevřen, uzavřen, znovu otevřen nebo upraven.
-settings.event_pull_request_assign=Požadavek na natažení přiřazen
-settings.event_pull_request_assign_desc=Požadavek na natažení přiřazen nebo nepřiřazen.
-settings.event_pull_request_label=Požadavek na natažení oštítkován
-settings.event_pull_request_label_desc=Štítky požadavku na natažení aktualizovány nebo vymazány.
-settings.event_pull_request_milestone=Požadavku na natažení přidán milník
-settings.event_pull_request_milestone_desc=Požadavku na natažení přidán nebo odebrán milník.
-settings.event_pull_request_comment=Požadavek na natažení okomentován
-settings.event_pull_request_comment_desc=Komentář požadavku na natažení vytvořen, upraven nebo odstraněn.
-settings.event_pull_request_review=Požadavek na natažení přezkoumán
-settings.event_pull_request_review_desc=Požadavek na natažení schválen, odmítnut nebo zkontrolován.
-settings.event_pull_request_sync=Požadavek na natažení synchronizován
-settings.event_pull_request_sync_desc=Požadavek na natažení synchronizován.
+settings.event_header_pull_request=Události pull requestu
+settings.event_pull_request=Pull request
+settings.event_pull_request_desc=Pull request otevřen, uzavřen, znovu otevřen nebo upraven.
+settings.event_pull_request_assign=Pull request přiřazen
+settings.event_pull_request_assign_desc=Pull request přiřazen nebo nepřiřazen.
+settings.event_pull_request_label=Pull request oštítkován
+settings.event_pull_request_label_desc=Štítky pull requestu aktualizovány nebo vymazány.
+settings.event_pull_request_milestone=Přidán milník pull requestu
+settings.event_pull_request_milestone_desc=Přidán nebo odebrán milník pull requestu.
+settings.event_pull_request_comment=Pull request okomentován
+settings.event_pull_request_comment_desc=Komentář pull requestu vytvořen, upraven nebo odstraněn.
+settings.event_pull_request_review=Pull request posouzen
+settings.event_pull_request_review_desc=Pull request schválen, odmítnut nebo zkontrolován.
+settings.event_pull_request_sync=Pull request synchronizován
+settings.event_pull_request_sync_desc=Pull request synchronizován.
+settings.event_pull_request_review_request=Požádáno o posouzení pull requestu
+settings.event_pull_request_review_request_desc=Přidána nebo ostraněna žádnost o kontrolu pull requestu.
+settings.event_pull_request_approvals=Schválení pull requestu
+settings.event_pull_request_merge=Sloučení pull requestu
 settings.event_package=Balíček
 settings.event_package_desc=Balíček vytvořen nebo odstraněn v repozitáři.
 settings.branch_filter=Filtr větví
@@ -2054,6 +2333,7 @@ settings.title=Název
 settings.deploy_key_content=Obsah
 settings.key_been_used=Klíč pro nasazení se stejným obsahem je již používán.
 settings.key_name_used=Klíč pro nasazení se stejným jménem již existuje.
+settings.add_key_success=Klíč pro nasazení „%s“ byl přidán.
 settings.deploy_key_deletion=Odstranit klíč pro nasazení
 settings.deploy_key_deletion_desc=Odstranění klíče pro nasazení zruší jeho přístup k repozitáři. Pokračovat?
 settings.deploy_key_deletion_success=Klíč pro nasazení byl odstraněn.
@@ -2071,45 +2351,65 @@ settings.protect_disable_push=Zakázat nahrávání
 settings.protect_disable_push_desc=Žádné nahrávání do této větve nebude povoleno.
 settings.protect_enable_push=Povolit nahrávání
 settings.protect_enable_push_desc=Každý, kdo má přístup k zápisu, bude moci nahrávat do této větve (ale ne vynucená nahrávání).
+settings.protect_enable_merge=Povolit sloučení
+settings.protect_enable_merge_desc=Každému, kdo má přístup k zápisu, bude povoleno sloučit pull requesty do této větve.
 settings.protect_whitelist_committers=Povolit nahrání jen vyjmenovaným
 settings.protect_whitelist_committers_desc=Pouze povolení uživatelé budou moci nahrávat do této větve (ale ne vynucení nahrávání).
 settings.protect_whitelist_deploy_keys=Povolit nahrání klíčům pro nasazení s přístupem pro zápis.
 settings.protect_whitelist_users=Povolení uživatelé pro nahrávání:
-settings.protect_whitelist_search_users=Hledat uživatele…
 settings.protect_whitelist_teams=Povolené týmy pro nahrávání:
-settings.protect_whitelist_search_teams=Vyhledat týmy…
 settings.protect_merge_whitelist_committers=Povolit vyjmenovaným slučování
-settings.protect_merge_whitelist_committers_desc=Povolit pouze vyjmenovaným uživatelům nebo týmům slučovat požadavky na natažení do této větve.
+settings.protect_merge_whitelist_committers_desc=Povolit pouze vyjmenovaným uživatelům nebo týmům slučovat pull requesty do této větve.
 settings.protect_merge_whitelist_users=Povolení uživatelé pro slučování:
 settings.protect_merge_whitelist_teams=Povolené týmy pro slučování:
 settings.protect_check_status_contexts=Povolit kontrolu stavu
+settings.protect_status_check_patterns=Vzorce kontroly stavu:
+settings.protect_status_check_patterns_desc=Zadejte vzory pro určení, které kontroly stavu musí projít před sloučením větví do větve, která odpovídá tomuto pravidlu. Každý řádek určuje vzor. Vzory nemohou být prázdné.
 settings.protect_check_status_contexts_desc=Požadovat kontrolu stavu před sloučením. Vyberte, jaké kontroly stavu musí projít před tím, než je možné větev sloučit do větve, která vyhovuje tomuto pravidlu. Pokud je povoleno, revize musí být nejprve nahrány do jiné větve, projít kontrolou stavu, a následné sloučeny nebo přímo nahrány do větve, která vyhovuje tomuto pravidlu. Pokud nejsou vybrány žádné kontexty, musí být poslední potvrzení úspěšné bez ohledu na kontext.
 settings.protect_check_status_contexts_list=Kontroly stavu pro tento repozitář zjištěné během posledního týdne
+settings.protect_status_check_matched=Odpovídá
+settings.protect_invalid_status_check_pattern=Neplatný vzor kontroly stavu: „%s“.
+settings.protect_no_valid_status_check_patterns=Žádné platné vzory kontroly stavu.
 settings.protect_required_approvals=Požadovaná schválení:
-settings.protect_required_approvals_desc=Umožnit sloučení pouze požadavkům na natažení s dostatečným pozitivním hodnocením.
+settings.protect_required_approvals_desc=Umožnit sloučení pouze pull requestů s dostatečným pozitivním hodnocením.
 settings.protect_approvals_whitelist_enabled=Omezit schválení na povolené uživatele nebo týmy
 settings.protect_approvals_whitelist_enabled_desc=Do požadovaných schválení se započítají pouze posouzení od povolených uživatelů nebo týmů. Bez seznamu povolených se započítává schválení od kohokoli s právem zápisu.
 settings.protect_approvals_whitelist_users=Povolení posuzovatelé:
 settings.protect_approvals_whitelist_teams=Povolené týmy pro posuzování:
 settings.dismiss_stale_approvals=Odmítnout nekvalitní schválení
-settings.dismiss_stale_approvals_desc=Pokud budou do větve nahrány nové revize, které mění obsah tohoto požadavku na natažení, všechna stará schválení budou zamítnuta.
+settings.dismiss_stale_approvals_desc=Pokud budou do větve nahrány nové revize, které mění obsah tohoto pull requestu, všechna stará schválení budou zamítnuta.
+settings.ignore_stale_approvals=Ignorovat zastaralá schválení
+settings.ignore_stale_approvals_desc=Nezapočítávejte schválení, která byla provedena u starších commitů (zastaralých recenzí), do počtu schválení, která má PR. Pokud jsou zastaralá hodnocení již zamítnuta, je to irelevantní.
 settings.require_signed_commits=Vyžadovat podepsané revize
 settings.require_signed_commits_desc=Odmítnout nahrání do této větve pokud nejsou podepsaná nebo jsou neověřitelná.
+settings.protect_branch_name_pattern=Vzor jména chráněných větví
+settings.protect_branch_name_pattern_desc=Vzory jmen chráněných větví. Pro vzorovou syntaxi viz <a href="https://github.com/gobwas/glob">dokumentace</a>. Příklady: main, release/**
+settings.protect_patterns=Vzory
+settings.protect_protected_file_patterns=Vzory chráněných souborů (oddělené středníkem „;“):
+settings.protect_protected_file_patterns_desc=Chráněné soubory, které nemají povoleno být měněny přímo, i když uživatel má právo přidávat, upravovat nebo mazat soubory v této větvi. Více vzorů lze oddělit pomocí středníku („;“). Podívejte se na <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> dokumentaci pro syntaxi vzoru. Příklady: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
+settings.protect_unprotected_file_patterns=Vzory nechráněných souborů (oddělené středníkem „;“):
+settings.protect_unprotected_file_patterns_desc=Nechráněné soubory, které je možné měnit přímo, pokud má uživatel právo zápisu, čímž se obejde omezení push. Více vzorů lze oddělit pomocí středníku („;“). Podívejte se na <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> dokumentaci pro syntaxi vzoru. Příklady: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
 settings.add_protected_branch=Zapnout ochranu
 settings.delete_protected_branch=Vypnout ochranu
+settings.update_protect_branch_success=Ochrana větví pro větev „%s“ byla aktualizována.
+settings.remove_protected_branch_success=Ochrana větví pro větev „%s“ byla zakázána.
+settings.remove_protected_branch_failed=Odstranění ochranného pravidla větve „%s“ se nezdařilo.
 settings.protected_branch_deletion=Zakázat ochranu větví
 settings.protected_branch_deletion_desc=Zakázání ochrany větví umožní uživatelům s právem zápisu nahrávat do této větve. Pokračovat?
 settings.block_rejected_reviews=Blokovat sloučení při zamítavých posouzeních
 settings.block_rejected_reviews_desc=Slučování nebude možné, pokud o změny požádají oficiální posuzovatelé, i když je k dispozici dostatek schválení.
 settings.block_on_official_review_requests=Blokovat sloučení při oficiální žádosti o posouzení
 settings.block_on_official_review_requests_desc=Slučování nebude možné, pokud mají oficiální požadavek na posouzení, i když mají k dispozici dostatek schválení.
-settings.block_outdated_branch=Blokovat sloučení, pokud je požadavek na natažení zastaralý
+settings.block_outdated_branch=Blokovat sloučení, pokud je pull request zastaralý
 settings.block_outdated_branch_desc=Slučování nebude možné, pokud je hlavní větev za základní větví.
-settings.default_branch_desc=Vybrat výchozí větev repozitáře pro požadavky na natažení a revize kódu:
+settings.default_branch_desc=Vybrat výchozí větev repozitáře pro pull requesty a revize kódu:
+settings.merge_style_desc=Sloučit styly
 settings.default_merge_style_desc=Výchozí styl sloučení pro požadavky na natažení:
 settings.choose_branch=Vyberte větev…
 settings.no_protected_branch=Nejsou tu žádné chráněné větve.
 settings.edit_protected_branch=Upravit
+settings.protected_branch_required_rule_name=Požadovaný název pravidla
+settings.protected_branch_duplicate_rule_name=Duplikovat název pravidla
 settings.protected_branch_required_approvals_min=Požadovaná schválení nesmí být záporné číslo.
 settings.tags=Značky
 settings.tags.protection=Ochrana značek
@@ -2120,18 +2420,27 @@ settings.tags.protection.allowed.teams=Povolené týmy
 settings.tags.protection.allowed.noone=Nikdo
 settings.tags.protection.create=Chránit značku
 settings.tags.protection.none=Neexistují žádné chráněné značky.
+settings.tags.protection.pattern.description=Můžete použít jediné jméno nebo vzor glob nebo regulární výraz, který bude odpovídat více značek. Přečtěte si více v <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">průvodci chráněnými značkami</a>.
 settings.bot_token=Token pro robota
 settings.chat_id=ID chatu
+settings.thread_id=ID vlákna
 settings.matrix.homeserver_url=URL adresa Homeserveru
 settings.matrix.room_id=ID místnosti
 settings.matrix.message_type=Typ zprávy
 settings.archive.button=Archivovat repozitář
 settings.archive.header=Archivovat tento repozitář
+settings.archive.text=Archivace repozitáře způsobí, že bude zcela určen pouze pro čtení. Bude skryt z ovládacího panelu. Nikdo (ani vy!) nebude moci vytvářet nové revize ani otevírat nové úkoly nebo pull requesty.
 settings.archive.success=Repozitář byl úspěšně archivován.
 settings.archive.error=Nastala chyba při archivování repozitáře. Prohlédněte si záznam pro více detailů.
 settings.archive.error_ismirror=Nemůžete archivovat zrcadlený repozitář.
 settings.archive.branchsettings_unavailable=Nastavení větví není dostupné, pokud je repozitář archivovaný.
 settings.archive.tagsettings_unavailable=Nastavení značek není k dispozici, pokud je repozitář archivován.
+settings.archive.mirrors_unavailable=Zrcadla nejsou k dispozici, pokud je repozitář archivován.
+settings.unarchive.button=Obnovit repozitář
+settings.unarchive.header=Obnovit tento repozitář
+settings.unarchive.text=Obnovení repozitáře vrátí možnost přijímání commitů a nahrávání. Stejně tak se obnoví i možnost zadávání nových úkolů a pull requestů.
+settings.unarchive.success=Repozitář byl úspěšně obnoven.
+settings.unarchive.error=Nastala chyba při obnovování repozitáře. Prohlédněte si záznam pro více detailů.
 settings.update_avatar_success=Avatar repozitáře byl aktualizován.
 settings.lfs=LFS
 settings.lfs_filelist=LFS soubory uložené v tomto repozitáři
@@ -2198,6 +2507,7 @@ diff.show_more=Zobrazit více
 diff.load=Načíst rozdílové porovnání
 diff.generated=vygenerováno
 diff.vendored=vendorováno
+diff.comment.add_line_comment=Přidat jednořádkový komentář
 diff.comment.placeholder=Zanechat komentář
 diff.comment.markdown_info=Je podporována úprava vzhledu pomocí markdown.
 diff.comment.add_single_comment=Přidat jeden komentář
@@ -2209,7 +2519,9 @@ diff.review.header=Odeslat posouzení
 diff.review.placeholder=Posoudit komentář
 diff.review.comment=Okomentovat
 diff.review.approve=Schválit
+diff.review.self_reject=Autoři pull requestu nemohou požadovat změny na svém vlastním pull requestu
 diff.review.reject=Požadovat změny
+diff.review.self_approve=Autoři pull requestu nemohou schválit svůj vlastní pull request
 diff.committed_by=odevzdal
 diff.protected=Chráněno
 diff.image.side_by_side=Vedle sebe
@@ -2231,13 +2543,18 @@ release.compare=Porovnat
 release.edit=upravit
 release.ahead.commits=<strong>%d</strong> revizí
 release.ahead.target=do %s od tohoto vydání
+tag.ahead.target=do %s od této značky
 release.source_code=Zdrojový kód
 release.new_subheader=Vydání organizuje verze projektu.
 release.edit_subheader=Vydání organizuje verze projektu.
 release.tag_name=Název značky
 release.target=Cíl
 release.tag_helper=Vyberte existující značku nebo vytvořte novou značku.
+release.tag_helper_new=Nová značka. Tato značka bude vytvořen z cíle.
+release.tag_helper_existing=Stávající značka.
 release.title=Název vydání
+release.title_empty=Název nesmí být prázdný.
+release.message=Popište toto vydání
 release.prerelease_desc=Označit jako předběžná verze
 release.prerelease_helper=Označit vydání jako nevhodné pro produkční nasazení.
 release.cancel=Zrušit
@@ -2247,6 +2564,7 @@ release.edit_release=Aktualizovat vydání
 release.delete_release=Smazat vydání
 release.delete_tag=Smazat značku
 release.deletion=Smazat vydání
+release.deletion_desc=Smazání vydání jej pouze odebere z Gitea. Nebude to mít vliv na značku Git, obsah vašeho repozitáře nebo jeho historii. Pokračovat?
 release.deletion_success=Vydání bylo odstraněno.
 release.deletion_tag_desc=Odstraní tuto značku z repozitáře. Obsah repozitáře a historie zůstanou nezměněny. Pokračovat?
 release.deletion_tag_success=Značka byla odstraněna.
@@ -2258,32 +2576,55 @@ release.downloads=Soubory ke stažení
 release.download_count=Stažení: %s
 release.add_tag_msg=Použít název a obsah vydání jako zprávu značky.
 release.add_tag=Vytvořit pouze značku
+release.releases_for=Vydání pro %s
 release.tags_for=Značky pro %s
 
 branch.name=Jméno větve
+branch.already_exists=Větev pojmenovaná „%s“ již existuje.
 branch.delete_head=Smazat
+branch.delete=Smazat větev „%s“
 branch.delete_html=Smazat větev
+branch.delete_desc=Smazání větve je trvalé. Přestože zrušená větev může existovat i po krátkou dobu, než bude skutečně odstraněna, NELZE ji většinou vrátit. Pokračovat?
+branch.deletion_success=Větev „%s“ byla smazána.
+branch.deletion_failed=Nepodařilo se odstranit větev „%s“.
+branch.delete_branch_has_new_commits=Větev „%s“ nemůže být smazána, protože byly přidány nové commity po sloučení.
 branch.create_branch=Vytvořit větev <strong>%s</strong>
+branch.create_from=z „%s“
+branch.create_success=Větev „%s“ byla vytvořena.
 branch.branch_already_exists=Větev „%s“ již existuje v tomto repozitáři.
+branch.branch_name_conflict=Jméno větve „%s“ koliduje s již existující větví „%s“.
+branch.tag_collision=Větev „%s“ nemůže být vytvořena, protože v repozitáři existuje značka se stejným jménem.
 branch.deleted_by=Odstranil %s
+branch.restore_success=Větev „%s“ byla obnovena.
+branch.restore_failed=Nepodařilo se obnovit větev „%s“.
+branch.protected_deletion_failed=Větev „%s“ je chráněna. Nemůže být smazána.
+branch.default_deletion_failed=Větev „%s“ je výchozí větev. Nelze ji odstranit.
+branch.restore=Obnovit větev „%s“
+branch.download=Stáhnout větev „%s“
+branch.rename=Přejmenovat větev „%s“
 branch.included_desc=Tato větev je součástí výchozí větve
 branch.included=Zahrnuje
 branch.create_new_branch=Vytvořit větev z větve:
 branch.confirm_create_branch=Vytvořit větev
+branch.warning_rename_default_branch=Přejmenováváte výchozí větev.
+branch.rename_branch_to=Přejmenovat „%s“ na:
 branch.confirm_rename_branch=Přejmenovat větev
 branch.create_branch_operation=Vytvořit větev
 branch.new_branch=Vytvořit novou větev
+branch.new_branch_from=Vytvořit novou větev z „%s“
 branch.renamed=Větev %s byla přejmenována na %s.
 
 tag.create_tag=Vytvořit značku <strong>%s</strong>
 tag.create_tag_operation=Vytvořit značku
 tag.confirm_create_tag=Vytvořit značku
+tag.create_tag_from=Vytvořit novou značku z „%s“
 
 tag.create_success=Značka „%s“ byla vytvořena.
 
 topic.manage_topics=Spravovat témata
 topic.done=Hotovo
 topic.count_prompt=Nelze vybrat více než 25 témat
+topic.format_prompt=Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a tečky („.“) a může být dlouhé až 35 znaků. Písmena musí být malá.
 
 find_file.go_to_file=Přejít na soubor
 find_file.no_matching=Nebyl nalezen žádný odpovídající soubor
@@ -2291,6 +2632,16 @@ find_file.no_matching=Nebyl nalezen žádný odpovídající soubor
 error.csv.too_large=Tento soubor nelze vykreslit, protože je příliš velký.
 error.csv.unexpected=Tento soubor nelze vykreslit, protože obsahuje neočekávaný znak na řádku %d ve sloupci %d.
 error.csv.invalid_field_count=Soubor nelze vykreslit, protože má nesprávný počet polí na řádku %d.
+error.broken_git_hook=Git háčky tohoto repozitáře se zdají být rozbité. Postupujte prosím podle <a target="_blank" rel="noreferrer" href="%s">dokumentace</a>, abyste je opravili, a poté nahrajte nějaké commity pro obnovení stavu.
+
+[graphs]
+component_loading=Načítání %s...
+component_loading_failed=Nelze načíst %s
+component_loading_info=Může to chvíli trvat…
+component_failed_to_load=Došlo k neočekávané chybě.
+code_frequency.what=frekvence kódu
+contributors.what=příspěvky
+recent_commits.what=nedávné commity
 
 [org]
 org_name_holder=Název organizace
@@ -2315,23 +2666,28 @@ team_permission_desc=Oprávnění
 team_unit_desc=Povolit přístup do částí repozitáře
 team_unit_disabled=(zakázaná)
 
+form.name_reserved=Název organizace „%s“ je rezervován.
+form.name_pattern_not_allowed=Vzor „%s“ není povolený v názvu organizace.
 form.create_org_not_allowed=Nemáte oprávnění vytvářet nové organizace.
 
 settings=Nastavení
 settings.options=Organizace
 settings.full_name=Celé jméno
+settings.email=Kontaktní e-mail
 settings.website=Webové stránky
 settings.location=Umístění
 settings.permission=Oprávnění
 settings.repoadminchangeteam=Správce úložišť může týmům přidávat a odebírat přístup
 settings.visibility=Viditelnost
 settings.visibility.public=Veřejná
+settings.visibility.limited=Omezeno (Viditelné pouze pro ověřené uživatele)
 settings.visibility.limited_shortname=Omezený
 settings.visibility.private=Soukromá (viditelné jen členům organizace)
 settings.visibility.private_shortname=Soukromý
 
 settings.update_settings=Upravit nastavení
 settings.update_setting_success=Nastavení organizace bylo upraveno.
+settings.change_orgname_prompt=Poznámka: Změna názvu organizace také změní adresu URL vaší organizace a uvolní staré jméno této organizace.
 settings.change_orgname_redirect_prompt=Staré jméno bude přesměrovávat, dokud nebude znovu obsazeno.
 settings.update_avatar_success=Avatar organizace byl aktualizován.
 settings.delete=Smazat organizaci
@@ -2391,14 +2747,15 @@ teams.write_permission_desc=Členství v tom týmu poskytuje právo <strong>záp
 teams.admin_permission_desc=Členství v tom týmu poskytuje právo <strong>správce</strong>: členové mohou číst z, nahrávat do a přidávat spolupracovníky do repozitářů týmu.
 teams.create_repo_permission_desc=Navíc tento tým uděluje oprávnění <strong>vytvořit repozitář</strong>: členové mohou vytvářet nové repozitáře v organizaci.
 teams.repositories=Repozitáře týmu
-teams.search_repo_placeholder=Hledat repozitář…
 teams.remove_all_repos_title=Odstranit všechny repozitáře týmu
 teams.remove_all_repos_desc=Tímto odeberete všechny repozitáře z týmu.
 teams.add_all_repos_title=Přidat všechny repozitáře
 teams.add_all_repos_desc=Tímto přidáte do týmu všechny repozitáře organizace.
+teams.add_nonexistent_repo=Repositář, který se snažíte přidat, neexistuje. Nejdříve jej vytvořte, prosím.
 teams.add_duplicate_users=Uživatel je již členem týmu.
 teams.repos.none=Tento tým nemůže přistoupit k žádným repozitářům.
 teams.members.none=Žádní členové v tomto týmu.
+teams.members.blocked_user=Nelze přidat uživatele, protože je zablokován organizací.
 teams.specific_repositories=Konkrétní repozitáře
 teams.specific_repositories_helper=Členové budou mít přístup pouze do repozitářů výslovně přidaných do týmu. Výběrem tohoto <strong>nebudou</strong> automaticky odstraněny již přidané repozitáře pomocí <i>Všechny repozitáře</i>.
 teams.all_repositories=Všechny repozitáře
@@ -2406,39 +2763,48 @@ teams.all_repositories_helper=Tým má přístup ke všem repositářům. Výbě
 teams.all_repositories_read_permission_desc=Tomuto týmu je udělen přístup pro <strong>Čtení</strong> <strong>všech repozitářů</strong>: členové mohou prohlížet a klonovat repozitáře.
 teams.all_repositories_write_permission_desc=Tomuto týmu je udělen přístup pro <strong>Zápis</strong> do <strong>všech repozitářů</strong>: členové mohou prohlížet a nahrávat do repozitářů.
 teams.all_repositories_admin_permission_desc=Tomuto týmu je udělen <strong>Administrátorský</strong> přístup do <strong>všech repozitářů</strong>: členové mohou prohlížet, nahrávat a přidávat spolupracovníky do repozitářů.
+teams.invite.title=Byli jste pozváni do týmu <strong>%s</strong> v organizaci <strong>%s</strong>.
 teams.invite.by=Pozvání od %s
 teams.invite.description=Pro připojení k týmu klikněte na tlačítko níže.
 
 [admin]
 dashboard=Přehled
+self_check=Samokontrola
+identity_access=Identita a přístup
 users=Uživatelské účty
 organizations=Organizace
 repositories=Repozitáře
 hooks=Webové háčky
+integrations=Integrace
 authentication=Zdroje ověření
 emails=Uživatelské e-maily
 config=Nastavení
+config_summary=Souhrn
+config_settings=Nastavení
 notices=Systémová oznámení
 monitor=Sledování
 first_page=První
 last_page=Poslední
 total=Celkem: %d
+settings=Nastavení správce
 
+dashboard.new_version_hint=Gitea %s je nyní k dispozici, právě u vás běži %s. Podívej se na <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blogu</a> pro více informací.
 dashboard.statistic=Souhrn
-dashboard.operations=Operace údržby
 dashboard.system_status=Status systému
 dashboard.operation_name=Název operace
 dashboard.operation_switch=Přepnout
 dashboard.operation_run=Spustit
 dashboard.clean_unbind_oauth=Vyčistit neprovázané OAuth spojení
 dashboard.clean_unbind_oauth_success=Všechna neprovázaná OAuth spojení byla smazána.
-dashboard.task.started=Zahájen úkol: %[1]s
-dashboard.task.process=Úkol: %[1]s
-dashboard.task.error=Chyba v úkolu: %[1]s: %[3]s
-dashboard.task.finished=Úkol: %[1]s začalo %[2]s skončilo
-dashboard.task.unknown=Neznámý úkol: %[1]s
+dashboard.task.started=Zahájena úloha: %[1]s
+dashboard.task.process=Úloha: %[1]s
+dashboard.task.cancelled=Úloha: %[1]s zrušean: %[3]s
+dashboard.task.error=Chyba v úloze: %[1]s: %[3]s
+dashboard.task.finished=Úloha: %[1]s začala %[2]s skončila
+dashboard.task.unknown=Neznámá úloha: %[1]s
 dashboard.cron.started=Začala naplánovaná úloha: %[1]s
 dashboard.cron.process=Naplánovaná úloha: %[1]s
+dashboard.cron.cancelled=Naplánovaná úloha: %[1]s zrušena: %[3]s
 dashboard.cron.error=Chyba v naplánované úloze: %s: %[3]s
 dashboard.cron.finished=Naplánovaná úloha: %[1]s skončila
 dashboard.delete_inactive_accounts=Smazat všechny neaktivované účty
@@ -2448,6 +2814,8 @@ dashboard.delete_repo_archives.started=Spuštěna úloha smazání všech archiv
 dashboard.delete_missing_repos=Smazat všechny repozitáře, které nemají Git soubory
 dashboard.delete_missing_repos.started=Spuštěna úloha mazání všech repozitářů, které nemají Git soubory.
 dashboard.delete_generated_repository_avatars=Odstranit vygenerované avatary repozitářů
+dashboard.sync_repo_branches=Synchronizovat chybějící větve z git dat do databází
+dashboard.sync_repo_tags=Synchronizovat značky z git dat do databáze
 dashboard.update_mirrors=Aktualizovat zrcadla
 dashboard.repo_health_check=Kontrola stavu všech repozitářů
 dashboard.check_repo_stats=Zkontrolovat všechny statistiky repositáře
@@ -2462,6 +2830,7 @@ dashboard.reinit_missing_repos=Znovu inicializovat všechny chybějící repozit
 dashboard.sync_external_users=Synchronizovat externí uživatelská data
 dashboard.cleanup_hook_task_table=Vyčistit tabulku hook_task
 dashboard.cleanup_packages=Vyčistit prošlé balíčky
+dashboard.cleanup_actions=Vyčištění prošlých záznamů a artefaktů z akcí
 dashboard.server_uptime=Doba provozu serveru
 dashboard.current_goroutine=Aktuální Goroutines
 dashboard.current_memory_usage=Aktuální využití paměti
@@ -2495,9 +2864,14 @@ dashboard.delete_old_actions=Odstranit všechny staré akce z databáze
 dashboard.delete_old_actions.started=Začalo odstraňování všech starých akcí z databáze.
 dashboard.update_checker=Kontrola aktualizací
 dashboard.delete_old_system_notices=Odstranit všechna stará systémová upozornění z databáze
-dashboard.stop_zombie_tasks=Zastavit zombie úkoly
-dashboard.stop_endless_tasks=Zastavit nekonečné úkoly
+dashboard.gc_lfs=Úklid LFS meta objektů
+dashboard.stop_zombie_tasks=Zastavit zombie úlohy
+dashboard.stop_endless_tasks=Zastavit nekonečné úlohy
 dashboard.cancel_abandoned_jobs=Zrušit opuštěné úlohy
+dashboard.start_schedule_tasks=Spustit naplánované úlohy
+dashboard.sync_branch.started=Synchronizace větví byla spuštěna
+dashboard.sync_tag.started=Synchronizace značek spuštěna
+dashboard.rebuild_issue_indexer=Znovu sestavit index úkolů
 
 users.user_manage_panel=Správa uživatelských účtů
 users.new_account=Vytvořit uživatelský účet
@@ -2506,12 +2880,16 @@ users.full_name=Celé jméno
 users.activated=Aktivován
 users.admin=Správce
 users.restricted=Omezený
+users.reserved=Rezervováno
+users.bot=Bot
+users.remote=Vzdálený
 users.2fa=2FA
 users.repos=Repozitáře
 users.created=Vytvořen
 users.last_login=Poslední přihlášení
 users.never_login=Nikdy nepřihlášen
 users.send_register_notify=Odeslat upozornění při registraci uživatele
+users.new_success=Uživatelský účet „%s“ byl vytvořen.
 users.edit=Upravit
 users.auth_source=Zdroj ověřování
 users.local=Místní
@@ -2536,6 +2914,7 @@ users.still_own_repo=Tento uživatel stále vlastní jeden nebo více repozitá
 users.still_has_org=Uživatel je člen organizace. Nejprve odstraňte uživatele ze všech organizací.
 users.purge=Vymazat uživatele
 users.purge_help=Vynuceně smazat uživatele a všechny repositáře, organizace a balíčky vlastněné uživatelem. Všechny komentáře budou také smazány.
+users.still_own_packages=Tento uživatel stále vlastní jeden nebo více balíčků, nejprve odstraňte tyto balíčky.
 users.deletion_success=Uživatelský účet byl smazán.
 users.reset_2fa=Resetovat 2FA
 users.list_status_filter.menu_text=Filtr
@@ -2550,6 +2929,7 @@ users.list_status_filter.is_prohibit_login=Zakázat přihlášení
 users.list_status_filter.not_prohibit_login=Povolit přihlášení
 users.list_status_filter.is_2fa_enabled=2FA povoleno
 users.list_status_filter.not_2fa_enabled=2FA zakázáno
+users.details=Detaily uživatele
 
 emails.email_manage_panel=Správa e-mailů uživatele
 emails.primary=Hlavní
@@ -2562,6 +2942,7 @@ emails.updated=E-mail aktualizován
 emails.not_updated=Aktualizace požadované e-mailové adresy se nezdařila: %v
 emails.duplicate_active=Tato e-mailová adresa je již aktivní pro jiného uživatele.
 emails.change_email_header=Aktualizovat vlastnosti e-mailu
+emails.change_email_text=Opravdu chcete aktualizovat tuto e-mailovou adresu?
 
 orgs.org_manage_panel=Správa organizací
 orgs.name=Název
@@ -2575,14 +2956,15 @@ repos.unadopted.no_more=Nebyly nalezeny žádné další nepřijaté repositář
 repos.owner=Vlastník
 repos.name=Název
 repos.private=Soukromý
-repos.watches=Sledovače
-repos.stars=Oblíbení
-repos.forks=Rozštěpení
 repos.issues=Úkoly
 repos.size=Velikost
+repos.lfs_size=Velikost LFS
 
 packages.package_manage_panel=Správa balíčků
 packages.total_size=Celková velikost: %s
+packages.unreferenced_size=Neodkazovaná velikost: %s
+packages.cleanup=Vyčistit prošlá data
+packages.cleanup.success=Úspěšné vyčištění dat, jejichž platnost vypršela
 packages.owner=Vlastník
 packages.creator=Tvůrce
 packages.name=Název
@@ -2593,10 +2975,12 @@ packages.size=Velikost
 packages.published=Publikováno
 
 defaulthooks=Výchozí webové háčky
+defaulthooks.desc=Webové háčky automaticky vytvářejí HTTP POST dotazy na server při určitých Gitea událostech. Webové háčky definované zde jsou výchozí a budou zkopírovány do všech nových repozitářů. Přečtěte si více v <a target="_blank" rel="noopener" href="https://docs.gitea.io/en-us/webhooks/">průvodci webovými háčky</a>.
 defaulthooks.add_webhook=Přidat výchozí webový háček
 defaulthooks.update_webhook=Aktualizovat výchozí webový háček
 
 systemhooks=Systémové webové háčky
+systemhooks.desc=Webové háčky automaticky vytvářejí HTTP POST dotazy na server při určitých Gitea událostech. Webové háčky definované zde budou vykonány na všech repozitářích systému, proto prosím zvažte jakékoli důsledky, které to může mít na výkon. Přečtěte si více v <a target="_blank" rel="noopener" href="https://docs.gitea.io/en-us/webhooks/">průvodci webovými háčky</a>.
 systemhooks.add_webhook=Přidat systémový webový háček
 systemhooks.update_webhook=Aktualizovat systémový webový háček
 
@@ -2674,6 +3058,7 @@ auths.oauth2_required_claim_value_helper=Nastavte tuto hodnotu pro omezení při
 auths.oauth2_group_claim_name=Název tvrzení poskytující názvy skupin pro tento zdroj. (nepovinné)
 auths.oauth2_admin_group=Hodnota tvrzení pro skupinu uživatelů administrátorů. (Volitelné - vyžaduje název tvrzení výše)
 auths.oauth2_restricted_group=Hodnota tvrzení pro skupinu omezených uživatelů. (Volitelné - vyžaduje název tvrzení výše)
+auths.oauth2_map_group_to_team=Mapa uváděných skupin do organizačních týmů. (Volitelné - vyžaduje výše uvedené jméno)
 auths.oauth2_map_group_to_team_removal=Odebrat uživatele z synchronizovaných týmů, pokud uživatel nepatří do odpovídající skupiny.
 auths.enable_auto_register=Povolit zaregistrování se
 auths.sspi_auto_create_users=Automaticky vytvářet uživatele
@@ -2688,21 +3073,24 @@ auths.sspi_default_language=Výchozí jazyk uživatele
 auths.sspi_default_language_helper=Výchozí jazyk pro uživatele automaticky vytvořené pomocí SSPI auth metody. Pokud dáváte přednost automatickému zjištění jazyka, ponechte prázdné.
 auths.tips=Tipy
 auths.tips.oauth2.general=Ověřování OAuth2
+auths.tips.oauth2.general.tip=Při registraci nové OAuth2 autentizace by URL callbacku/přesměrování měla být:
 auths.tip.oauth2_provider=Poskytovatel OAuth2
 auths.tip.bitbucket=Vytvořte nového OAuth konzumenta na https://bitbucket.org/account/user/<vase-uzivatelske-jmeno>/oauth-consumers/new a přidejte oprávnění „Account“ - „Read“
 auths.tip.nextcloud=Zaregistrujte nového OAuth konzumenta na vaší instanci pomocí následujícího menu „Nastavení -> Zabezpečení -> OAuth 2.0 klient“
 auths.tip.dropbox=Vytvořte novou aplikaci na https://www.dropbox.com/developers/apps
 auths.tip.facebook=Registrujte novou aplikaci na https://developers.facebook.com/apps a přidejte produkt „Facebook Login“
 auths.tip.github=Registrujte novou OAuth aplikaci na https://github.com/settings/applications/new
-auths.tip.gitlab=Registrujte novou aplikaci na https://gitlab.com/profile/applications
+auths.tip.gitlab_new=Zaregistrujte novou aplikaci na https://gitlab.com/-/profile/applications
 auths.tip.google_plus=Získejte klientské pověření OAuth2 z Google API konzole na https://console.developers.google.com/
 auths.tip.openid_connect=Použijte OpenID URL pro objevování spojení (<server>/.well-known/openid-configuration) k nastavení koncových bodů
 auths.tip.twitter=Jděte na https://dev.twitter.com/apps, vytvořte aplikaci a ujistěte se, že volba „Allow this application to be used to Sign in with Twitter“ je povolená
 auths.tip.discord=Registrujte novou aplikaci na https://discordapp.com/developers/applications/me
+auths.tip.gitea=Registrovat novou Oauth2 aplikaci. Návod naleznete na https://docs.gitea.com/development/oauth2-provider
 auths.tip.yandex=Vytvořte novou aplikaci na https://oauth.yandex.com/client/new. Vyberte následující oprávnění z „Yandex.Passport API“ sekce: „Přístup k e-mailové adrese“, „Přístup k uživatelskému avataru“ a „Přístup k uživatelskému jménu, jménu a příjmení, pohlaví“
 auths.tip.mastodon=Vložte vlastní URL instance pro mastodon, kterou se chcete autentizovat (nebo použijte výchozí)
 auths.edit=Upravit zdroj ověřování
 auths.activated=Tento zdroj ověřování je aktivován
+auths.new_success=Zdroj ověřování „%s“ byl přidán.
 auths.update_success=Zdroj ověřování byl aktualizován.
 auths.update=Aktualizovat zdroj ověřování
 auths.delete=Smazat zdroj ověřování
@@ -2710,7 +3098,9 @@ auths.delete_auth_title=Smazat zdroj ověřování
 auths.delete_auth_desc=Zamezíte přihlášení uživatelům pomocí tohoto zdroje ověřování, pokud ho smažete. Pokračovat?
 auths.still_in_used=Zdroj ověřování je stále používán. Nejprve převeďte nebo smažte všechny uživatele, kteří používají tento způsob ověřování.
 auths.deletion_success=Zdroj ověřování byl smazán.
+auths.login_source_exist=Zdroj ověřování „%s“ již existuje.
 auths.login_source_of_type_exist=Zdroj ověřování tohoto typu již existuje.
+auths.unable_to_initialize_openid=Nelze inicializovat poskytovatele OpenID Connect: %s
 auths.invalid_openIdConnectAutoDiscoveryURL=Neplatná URL adresa pro automatické vyhledání (musí být platná adresa URL začínající http:// nebo https://)
 
 config.server_config=Nastavení serveru
@@ -2725,6 +3115,7 @@ config.disable_router_log=Vypnout log směrovače
 config.run_user=Spustit jako uživatel
 config.run_mode=Režim spouštění
 config.git_version=Verze Gitu
+config.app_data_path=Cesta k datům aplikace
 config.repo_root_path=Kořenový adresář repozitářů
 config.lfs_root_path=Kořenový adresář LFS
 config.log_file_root_path=Adresář logů
@@ -2799,6 +3190,9 @@ config.mailer_sendmail_timeout=Časový limit Sandmail
 config.mailer_use_dummy=Fiktivní
 config.test_email_placeholder=E-mail (např.: test@example.com)
 config.send_test_mail=Odeslat zkušební e-mail
+config.send_test_mail_submit=Odeslat
+config.test_mail_failed=Odeslání testovacího e-mailu na „%s“ selhalo: %v
+config.test_mail_sent=Zkušební e-mail byl odeslán na „%s“.
 
 config.oauth_config=Nastavení ověření OAuth
 config.oauth_enabled=Zapnutý
@@ -2822,6 +3216,7 @@ config.picture_config=Nastavení obrázku a avataru
 config.picture_service=Služba ikon uživatelů
 config.disable_gravatar=Zakázat službu Gravatar
 config.enable_federated_avatar=Povolit avatary z veřejných zdrojů
+config.open_with_editor_app_help=Editory "Otevřít" v nabídce klon. Ponecháte-li prázdné, bude použito výchozí. Pro zobrazení výchozího nastavení rozbalte.
 
 config.git_config=Konfigurace Gitu
 config.git_disable_diff_highlight=Zakázat zvýraznění syntaxe v rozdílovém zobrazení
@@ -2836,12 +3231,15 @@ config.git_pull_timeout=Časový limit operace stažení
 config.git_gc_timeout=Časový limit operace GC
 
 config.log_config=Nastavení logů
+config.logger_name_fmt=Logger: %s
 config.disabled_logger=Zakázané
 config.access_log_mode=Režim logování přístupu
+config.access_log_template=Šablona záznamu přístupu
 config.xorm_log_sql=Logovat SQL
 
 config.set_setting_failed=Nastavení %s se nezdařilo
 
+monitor.stats=Statistiky
 
 monitor.cron=Naplánované úlohy
 monitor.name=Název
@@ -2851,6 +3249,8 @@ monitor.previous=Předešlý čas spuštění
 monitor.execute_times=Vykonání
 monitor.process=Spuštěné procesy
 monitor.stacktrace=Výpisy zásobníku
+monitor.processes_count=%d procesů
+monitor.download_diagnosis_report=Stáhnout diagnosttickou zprávu
 monitor.desc=Popis
 monitor.start=Čas zahájení
 monitor.execute_time=Doba provádění
@@ -2868,7 +3268,9 @@ monitor.queue.exemplar=Typ vzoru
 monitor.queue.numberworkers=Počet workerů
 monitor.queue.maxnumberworkers=Maximální počet workerů
 monitor.queue.numberinqueue=Číslo ve frontě
+monitor.queue.review_add=Posoudit / přidat workery
 monitor.queue.settings.title=Nastavení fondu
+monitor.queue.settings.desc=Fondy se dynamicky zvětšují v závislosti na blokování jejich pracovních front.
 monitor.queue.settings.maxnumberworkers=Maximální počet workerů
 monitor.queue.settings.maxnumberworkers.placeholder=V současné době %[1]d
 monitor.queue.settings.maxnumberworkers.error=Maximální počet workerů musí být číslo
@@ -2892,6 +3294,13 @@ notices.desc=Popis
 notices.op=Akce
 notices.delete_success=Systémové upozornění bylo smazáno.
 
+self_check.no_problem_found=Zatím nebyl nalezen žádný problém.
+self_check.database_collation_mismatch=Očekávejte, že databáze použije collation: %s
+self_check.database_collation_case_insensitive=Databáze používá collation %s, což je collation nerozlišující velká a malá písmena. Ačkoli s ní Gitea může pracovat, mohou se vyskytnout vzácné případy, kdy nebude fungovat podle očekávání.
+self_check.database_inconsistent_collation_columns=Databáze používá collation %s, ale tyto sloupce používají chybné collation. To může způsobit neočekávané problémy.
+self_check.database_fix_mysql=Pro uživatele MySQL/MariaDB můžete použít příkaz "gitea doctor convert", který opraví problémy s collation, nebo můžete také problém vyřešit příkazem "ALTER ... COLLATE ..." SQL ručně.
+self_check.database_fix_mssql=Uživatelé MSSQL mohou problém vyřešit pouze pomocí příkazu "ALTER ... COLLATE ..." SQL ručně.
+
 [action]
 create_repo=vytvořil/a repozitář <a href="%s">%s</a>
 rename_repo=přejmenoval/a repozitář z <code>%[1]s</code> na <a href="%[2]s">%[3]s</a>
@@ -2899,13 +3308,13 @@ commit_repo=nahrál/a do <a href="%[2]s">%[3]s</a> v <a href="%[1]s">%[4]s</a>
 create_issue=`otevřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>`
 close_issue=`uzavřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>`
 reopen_issue=`znovuotevřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>`
-create_pull_request=`vytvořil/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
-close_pull_request=`uzavřel/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
-reopen_pull_request=`znovuotevřel/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
+create_pull_request=`vytvořil/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
+close_pull_request=`uzavřel/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
+reopen_pull_request=`znovuotevřel/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
 comment_issue=`okomentoval/a problém <a href="%[1]s">%[3]s#%[2]s</a>`
-comment_pull=`okomentoval/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
-merge_pull_request=`sloučil/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
-auto_merge_pull_request=`automaticky sloučen požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
+comment_pull=`okomentoval/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
+merge_pull_request=`sloučil/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
+auto_merge_pull_request=`automaticky sloučen pull request <a href="%[1]s">%[3]s#%[2]s</a>`
 transfer_repo=předal/a repozitář <code>%s</code> uživateli/organizaci <a href="%s">%s</a>
 push_tag=nahrál/a značku <a href="%[2]s">%[3]s</a> do <a href="%[1]s">%[4]s</a>
 delete_tag=smazal/a značku %[2]s z <a href="%[1]s">%[3]s</a>
@@ -2987,6 +3396,7 @@ desc=Správa balíčků repozitáře.
 empty=Zatím nejsou žádné balíčky.
 empty.documentation=Další informace o registru balíčků naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>.
 empty.repo=Nahráli jste balíček, ale nezobrazil se zde? Přejděte na <a href="%[1]s">nastavení balíčku</a> a propojte jej s tímto repozitářem.
+registry.documentation=Další informace o registru %s naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>.
 filter.type=Typ
 filter.type.all=Vše
 filter.no_result=Váš filtr nepřinesl žádné výsledky.
@@ -3050,6 +3460,7 @@ debian.repository.distributions=Distribuce
 debian.repository.components=Komponenty
 debian.repository.architectures=Architektury
 generic.download=Stáhnout balíček z příkazové řádky:
+go.install=Nainstalujte balíček z příkazové řádky:
 helm.registry=Nastavte tento registr z příkazového řádku:
 helm.install=Pro instalaci balíčku spusťte následující příkaz:
 maven.registry=Nastavte tento registr ve vašem projektu <code>pom.xml</code> souboru:
@@ -3071,7 +3482,12 @@ pub.install=Chcete-li nainstalovat balíček pomocí Dart, spusťte následujíc
 pypi.requires=Vyžaduje Python
 pypi.install=Pro instalaci balíčku pomocí pip spusťte následující příkaz:
 rpm.registry=Nastavte tento registr z příkazového řádku:
+rpm.distros.redhat=na distribuce založené na RedHat
+rpm.distros.suse=na distribuce založené na SUSE
 rpm.install=Pro instalaci balíčku spusťte následující příkaz:
+rpm.repository=Informace o repozitáři
+rpm.repository.architectures=Architektury
+rpm.repository.multiple_groups=Tento balíček je k dispozici ve více skupinách.
 rubygems.install=Pro instalaci balíčku pomocí gem spusťte následující příkaz:
 rubygems.install2=nebo ho přidejte do Gemfie:
 rubygems.dependencies.runtime=Běhové závislosti
@@ -3079,6 +3495,8 @@ rubygems.dependencies.development=Vývojové závislosti
 rubygems.required.ruby=Vyžaduje verzi Ruby
 rubygems.required.rubygems=Vyžaduje verzi RubyGem
 swift.registry=Nastavte tento registr z příkazového řádku:
+swift.install=Přidejte balíček do svého <code>Package.swift</code> souboru:
+swift.install2=a spustit následující příkaz:
 vagrant.install=Pro přidání Vagrant box spusťte následující příkaz:
 settings.link=Propojit tento balíček s repozitářem
 settings.link.description=Pokud propojíte balíček s repozitářem, je tento balíček uveden v seznamu balíčků repozitáře.
@@ -3093,14 +3511,17 @@ settings.delete.success=Balíček byl odstraněn.
 settings.delete.error=Nepodařilo se odstranit balíček.
 owner.settings.cargo.title=Index registru Cargo
 owner.settings.cargo.initialize=Inicializovat index
+owner.settings.cargo.initialize.description=Pro použití Cargo registru je zapotřebí speciální index Git. Použití této možnosti (znovu)vytvoří repozitář a automaticky jej nastaví.
 owner.settings.cargo.initialize.error=Nepodařilo se inicializovat Cargo index: %v
 owner.settings.cargo.initialize.success=Index Cargo byl úspěšně vytvořen.
 owner.settings.cargo.rebuild=Znovu vytvořit Index
+owner.settings.cargo.rebuild.description=Obnova může být užitečná, pokud index není synchronizován s uloženými balíčky Cargo.
 owner.settings.cargo.rebuild.error=Obnovení Cargo indexu se nezdařilo: %v
 owner.settings.cargo.rebuild.success=Cargo Index byl úspěšně obnoven.
 owner.settings.cleanuprules.title=Spravovat pravidla pro čištění
 owner.settings.cleanuprules.add=Přidat pravidlo pro čištění
 owner.settings.cleanuprules.edit=Upravit pravidlo pro čištění
+owner.settings.cleanuprules.none=Nejsou k dispozici žádná pravidla čištění. Prohlédněte si prosím dokumentaci.
 owner.settings.cleanuprules.preview=Náhled pravidla pro čištění
 owner.settings.cleanuprules.preview.overview=%d balíčků má být odstraněno.
 owner.settings.cleanuprules.preview.none=Pravidlo čištění neodpovídá žádným balíčkům.
@@ -3119,6 +3540,7 @@ owner.settings.cleanuprules.success.update=Pravidlo pro čištění bylo aktuali
 owner.settings.cleanuprules.success.delete=Pravidlo pro čištění bylo odstraněno.
 owner.settings.chef.title=Registr Chef
 owner.settings.chef.keypair=Generovat pár klíčů
+owner.settings.chef.keypair.description=Pro autentizaci do registru Chef je zapotřebí pár klíčů. Pokud jste předtím vytvořili pár klíčů, nově vygenerovaný pár klíčů vyřadí starý pár klíčů.
 
 [secrets]
 secrets=Tajné klíče
@@ -3145,6 +3567,7 @@ status.waiting=Čekání
 status.running=Probíhá
 status.success=Úspěch
 status.failure=Chyba
+status.cancelled=Zrušeno
 status.skipped=Přeskočeno
 status.blocked=Blokováno
 
@@ -3158,7 +3581,8 @@ runners.description=Popis
 runners.labels=Štítky
 runners.last_online=Poslední čas online
 runners.runner_title=Runner
-runners.task_list=Nedávné úkoly na tomto runneru
+runners.task_list=Nedávné úlohy na tomto runneru
+runners.task_list.no_tasks=Zatím zde nejsou žádné úlohy.
 runners.task_list.run=Spustit
 runners.task_list.status=Status
 runners.task_list.repository=Repozitář
@@ -3172,23 +3596,68 @@ runners.delete_runner=Odstranit tento runner
 runners.delete_runner_success=Runner byl úspěšně odstraněn
 runners.delete_runner_failed=Odstranění runneru selhalo
 runners.delete_runner_header=Potvrdit odstranění tohoto runneru
+runners.delete_runner_notice=Pokud na tomto runneru běží úloha, bude ukončena a označena jako neúspěšná. Může dojít k přerušení vytváření pracovního postupu.
 runners.status.unspecified=Neznámý
 runners.status.idle=Nečinný
 runners.status.active=Aktivní
 runners.status.offline=Offline
 runners.version=Verze
+runners.reset_registration_token=Resetovat registrační token
+runners.reset_registration_token_success=Registrační token runneru byl úspěšně obnoven
 
 runs.all_workflows=Všechny pracovní postupy
 runs.commit=Commit
+runs.scheduled=Naplánováno
+runs.pushed_by=náhrán
+runs.invalid_workflow_helper=Konfigurační soubor pracovního postupu je neplatný. Zkontrolujte prosím konfigurační soubor: %s
+runs.no_matching_online_runner_helper=Žádný odpovídající online runner s popiskem: %s
+runs.no_job_without_needs=Pracovní postup musí obsahovat alespoň jednu úlohu bez závislostí.
+runs.actor=Aktér
 runs.status=Status
+runs.actors_no_select=Všichni aktéři
+runs.status_no_select=Všechny stavy
+runs.no_results=Nebyly nalezeny žádné výsledky.
+runs.no_workflows=Zatím neexistují žádné pracovní postupy.
+runs.no_workflows.quick_start=Nevíte jak začít s Gitea Actions? Podívejte se na <a target="_blank" rel="noopener noreferrer" href="%s">průvodce rychlým startem</a>.
+runs.no_workflows.documentation=Další informace o Gitea Actions naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>.
+runs.no_runs=Pracovní postup zatím nebyl spuštěn.
+runs.empty_commit_message=(prázdná zpráva commitu)
 
+workflow.disable=Zakázat pracovní postup
+workflow.disable_success=Pracovní postup „%s“ byl úspěšně deaktivován.
+workflow.enable=Povolit pracovní postup
+workflow.enable_success=Pracovní postup „%s“ byl úspěšně aktivován.
+workflow.disabled=Pracovní postup je zakázán.
 
+need_approval_desc=Potřebujete schválení pro spuštění pracovních postupů pro rozštěpený pull request.
 
+variables=Proměnné
+variables.management=Správa proměnných
+variables.creation=Přidat proměnnou
+variables.none=Zatím nejsou žádné proměnné.
+variables.deletion=Odstranit proměnnou
+variables.deletion.description=Odstranění proměnné je trvalé a nelze jej vrátit zpět. Pokračovat?
+variables.description=Proměnné budou předány určitým akcím a nelze je přečíst jinak.
+variables.id_not_exist=Proměnná s ID %d neexistuje.
+variables.edit=Upravit proměnnou
+variables.deletion.failed=Nepodařilo se odstranit proměnnou.
+variables.deletion.success=Proměnná byla odstraněna.
+variables.creation.failed=Přidání proměnné se nezdařilo.
+variables.creation.success=Proměnná „%s“ byla přidána.
+variables.update.failed=Úprava proměnné se nezdařila.
+variables.update.success=Proměnná byla upravena.
 
 [projects]
+type-1.display_name=Samostatný projekt
+type-2.display_name=Projekt repozitíře
 type-3.display_name=Projekt organizace
 
 [git.filemode]
+changed_filemode=%[1]s → %[2]s
 ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Adresář
+normal_file=Normální soubor
+executable_file=Spustitelný soubor
 symbolic_link=Symbolický odkaz
+submodule=Submodul
 
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index c24d25b1ac..9a09c2922e 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -17,6 +17,7 @@ template=Template
 language=Sprache
 notifications=Benachrichtigungen
 active_stopwatch=Aktive Zeiterfassung
+tracked_time_summary=Zusammenfassung der erfassten Zeit basierend auf Filtern der Issue-Liste
 create_new=Erstellen…
 user_profile_and_more=Profil und Einstellungen…
 signed_in_as=Angemeldet als
@@ -24,6 +25,7 @@ enable_javascript=Diese Website benötigt JavaScript.
 toc=Inhaltsverzeichnis
 licenses=Lizenzen
 return_to_gitea=Zurück zu Gitea
+more_items=Weitere Einträge
 
 username=Benutzername
 email=E-Mail-Adresse
@@ -90,6 +92,7 @@ remove=Löschen
 remove_all=Alle entfernen
 remove_label_str=Element "%s " entfernen
 edit=Bearbeiten
+view=Anzeigen
 
 enabled=Aktiviert
 disabled=Deaktiviert
@@ -99,7 +102,7 @@ copy=Kopieren
 copy_url=URL kopieren
 copy_hash=Hash kopieren
 copy_content=Inhalt kopieren
-copy_branch=Branchenname kopieren
+copy_branch=Branchnamen kopieren
 copy_success=Kopiert!
 copy_error=Kopieren fehlgeschlagen
 copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden
@@ -111,6 +114,7 @@ loading=Laden…
 error=Fehler
 error404=Die Seite, die Du versuchst aufzurufen, <strong>existiert nicht</strong> oder <strong>Du bist nicht berechtigt</strong>, diese anzusehen.
 go_back=Zurück
+invalid_data=Ungültige Daten: %v
 
 never=Niemals
 unknown=Unbekannt
@@ -121,6 +125,7 @@ pin=Anheften
 unpin=Loslösen
 
 artifacts=Artefakte
+confirm_delete_artifact=Bist du sicher, dass du das Artefakt '%s' löschen möchtest?
 
 archived=Archiviert
 
@@ -139,6 +144,43 @@ confirm_delete_selected=Alle ausgewählten Elemente löschen?
 name=Name
 value=Wert
 
+filter=Filter
+filter.clear=Filter leeren
+filter.is_archived=Archiviert
+filter.not_archived=Nicht archiviert
+filter.is_fork=Fork
+filter.not_fork=Kein Fork
+filter.is_mirror=Gespiegelt
+filter.not_mirror=Nicht gespiegelt
+filter.is_template=Template
+filter.not_template=Kein Template
+filter.public=Öffentlich
+filter.private=Privat
+
+no_results_found=Es wurden keine Ergebnisse gefunden.
+
+[search]
+search=Suche ...
+type_tooltip=Suchmodus
+fuzzy=Ähnlich
+fuzzy_tooltip=Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind
+match=Genau
+match_tooltip=Nur genau zum Suchbegriff passende Ergebnisse einbeziehen
+repo_kind=Repositories durchsuchen ...
+user_kind=Benutzer durchsuchen ...
+org_kind=Organisationen durchsuchen ...
+team_kind=Teams durchsuchen ...
+code_kind=Code durchsuchen ...
+code_search_unavailable=Zurzeit ist die Code-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
+code_search_by_git_grep=Aktuelle Code-Suchergebnisse werden von "git grep" bereitgestellt. Es könnte bessere Ergebnisse geben, wenn der Website-Administrator den Repository-Indexer aktiviert.
+package_kind=Pakete durchsuchen ...
+project_kind=Projekte durchsuchen ...
+branch_kind=Branches durchsuchen ...
+commit_kind=Commits durchsuchen ...
+runner_kind=Runner durchsuchen ...
+no_results=Es wurden keine passenden Ergebnisse gefunden.
+keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
+
 [aria]
 navbar=Navigationsleiste
 footer=Fußzeile
@@ -244,6 +286,7 @@ email_title=E-Mail-Einstellungen
 smtp_addr=SMTP-Host
 smtp_port=SMTP-Port
 smtp_from=E-Mail senden als
+smtp_from_invalid=Die „E-Mail senden als“ Adresse ist ungültig
 smtp_from_helper=E-Mail-Adresse, die von Gitea genutzt werden soll. Bitte gib die E-Mail-Adresse im Format „"Name" <email@example.com>“ ein.
 mailer_user=SMTP-Benutzername
 mailer_password=SMTP-Passwort
@@ -303,6 +346,7 @@ env_config_keys=Umgebungskonfiguration
 env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet:
 
 [home]
+nav_menu=Navigationsmenü
 uname_holder=E-Mail-Adresse oder Benutzername
 password_holder=Passwort
 switch_dashboard_context=Kontext der Übersichtsseite wechseln
@@ -312,7 +356,6 @@ collaborative_repos=Gemeinschaftliche Repositories
 my_orgs=Meine Organisationen
 my_mirrors=Meine Mirrors
 view_home=%s ansehen
-search_repos=Finde ein Repository…
 filter=Andere Filter
 filter_by_team_repositories=Nach Team-Repositories filtern
 feed_of=`Feed von "%s"`
@@ -333,20 +376,8 @@ issues.in_your_repos=Eigene Repositories
 repos=Repositories
 users=Benutzer
 organizations=Organisationen
-search=Suche
 go_to=Gehe zu
 code=Code
-search.type.tooltip=Suchmodus
-search.fuzzy=Ähnlich
-search.fuzzy.tooltip=Zeige auch Ergebnisse, die dem Suchbegriff ähneln
-search.match=Genau
-search.match.tooltip=Zeige nur Ergebnisse, die exakt mit dem Suchbegriff übereinstimmen
-code_search_unavailable=Derzeit ist die Code-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
-repo_no_results=Keine passenden Repositories gefunden.
-user_no_results=Keine passenden Benutzer gefunden.
-org_no_results=Keine passenden Organisationen gefunden.
-code_no_results=Es konnte kein passender Code für deinen Suchbegriff gefunden werden.
-code_search_results=`Suchergebnisse für "%s"`
 code_last_indexed_at=Zuletzt indexiert %s
 relevant_repositories_tooltip=Repositories, die Forks sind oder die kein Thema, kein Symbol und keine Beschreibung haben, werden ausgeblendet.
 relevant_repositories=Es werden nur relevante Repositories angezeigt, <a href="%s">ungefilterte Ergebnisse anzeigen</a>.
@@ -359,11 +390,12 @@ disable_register_prompt=Die Registrierung ist deaktiviert. Bitte wende dich an d
 disable_register_mail=E-Mail-Bestätigung bei der Registrierung ist deaktiviert.
 manual_activation_only=Kontaktiere den Website-Administrator, um die Aktivierung abzuschließen.
 remember_me=Dieses Gerät speichern
+remember_me.compromised=Das Login-Token ist nicht mehr gültig, was auf ein kompromittiertes Konto hindeuten kann. Bitte überprüfe dein Konto auf ungewöhnliche Aktivitäten.
 forgot_password_title=Passwort vergessen
 forgot_password=Passwort vergessen?
 sign_up_now=Noch kein Konto? Jetzt registrieren.
 sign_up_successful=Konto wurde erfolgreich erstellt. Willkommen!
-confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b> gesendet. Bitte überprüfe dein Postfach innerhalb der nächsten %s, um die Registrierung abzuschließen.
+confirmation_mail_sent_prompt_ex=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b>gesendet. Bitte überprüfe deinen Posteingang innerhalb der nächsten %s, um den Registrierungsprozess abzuschließen. Wenn deine Registrierungs-E-Mail-Adresse falsch ist, kannst du dich erneut anmelden und diese ändern.
 must_change_password=Aktualisiere dein Passwort
 allow_password_change=Verlange vom Benutzer das Passwort zu ändern (empfohlen)
 reset_password_mail_sent_prompt=Eine Bestätigungs-E-Mail wurde an <b>%s</b> gesendet. Bitte überprüfe dein Postfach innerhalb von %s, um den Wiederherstellungsprozess abzuschließen.
@@ -373,6 +405,7 @@ prohibit_login=Anmelden verboten
 prohibit_login_desc=Die Anmeldung mit diesem Konto ist nicht gestattet. Bitte kontaktiere den Administrator.
 resent_limit_prompt=Du hast bereits eine Aktivierungs-E-Mail angefordert. Bitte warte 3 Minuten und probiere es dann nochmal.
 has_unconfirmed_mail=Hallo %s, du hast eine unbestätigte E-Mail-Adresse (<b>%s</b>). Wenn du keine Bestätigungs-E-Mail erhalten hast oder eine neue senden möchtest, klicke bitte auf den folgenden Button.
+change_unconfirmed_mail_address=Wenn deine Registrierungs-E-Mail-Adresse falsch ist, kannst du sie hier ändern und eine neue Bestätigungs-E-Mail senden.
 resend_mail=Aktivierungs-E-Mail erneut verschicken
 email_not_associate=Diese E-Mail-Adresse ist mit keinem Konto verknüpft.
 send_reset_mail=Wiederherstellungs-E-Mail senden
@@ -420,6 +453,7 @@ authorization_failed_desc=Die Autorisierung ist fehlgeschlagen, da wir eine ung
 sspi_auth_failed=SSPI-Authentifizierung fehlgeschlagen
 password_pwned=Das von dir gewählte Passwort befindet sich auf einer <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">List gestohlener Passwörter</a>, die öffentlich verfügbar sind. Bitte versuche es erneut mit einem anderen Passwort und ziehe in Erwägung, auch anderswo deine Passwörter zu ändern.
 password_pwned_err=Anfrage an HaveIBeenPwned konnte nicht abgeschlossen werden
+last_admin=Du kannst den letzten Admin nicht entfernen. Es muss mindestens einen Administrator geben.
 
 [mail]
 view_it_on=Auf %s ansehen
@@ -552,6 +586,7 @@ team_name_been_taken=Der Teamname ist bereits vergeben.
 team_no_units_error=Das Team muss auf mindestens einen Bereich Zugriff haben.
 email_been_used=Die E-Mail-Adresse wird bereits verwendet.
 email_invalid=Die E-Mail-Adresse ist ungültig.
+email_domain_is_not_allowed=Die Domain der Benutzer-E-Mail <b>%s</b> steht im Widerspruch zu EMAIL_DOMAIN_ALLOWLIST oder EMAIL_DOMAIN_BLOCKLIST. Bitte stelle sicher, dass deine Operation erwartet ist.
 openid_been_used=Die OpenID-Adresse "%s" wird bereits verwendet.
 username_password_incorrect=Benutzername oder Passwort ist falsch.
 password_complexity=Das Passwort erfüllt nicht die Komplexitätsanforderungen:
@@ -563,6 +598,8 @@ enterred_invalid_repo_name=Der eingegebenen Repository-Name ist falsch.
 enterred_invalid_org_name=Der eingegebene Organisation-Name ist falsch.
 enterred_invalid_owner_name=Der Name des neuen Besitzers ist ungültig.
 enterred_invalid_password=Das eingegebene Passwort ist falsch.
+unset_password=Der Login-Benutzer hat das Passwort nicht gesetzt.
+unsupported_login_type=Der Anmeldetyp wird zum Löschen des Kontos nicht unterstützt.
 user_not_exist=Dieser Benutzer ist nicht vorhanden.
 team_not_exist=Dieses Team existiert nicht.
 last_org_owner=Du kannst den letzten Benutzer nicht aus dem 'Besitzer'-Team entfernen. Es muss mindestens einen Besitzer in einer Organisation geben.
@@ -585,6 +622,8 @@ org_still_own_packages=Diese Organisation besitzt noch ein oder mehrere Pakete,
 
 target_branch_not_exist=Der Ziel-Branch existiert nicht.
 
+admin_cannot_delete_self=Du kannst dich nicht selbst löschen, wenn du ein Administrator bist. Bitte entferne zuerst deine Administratorrechte.
+
 [user]
 change_avatar=Profilbild ändern…
 joined_on=Beigetreten am %s
@@ -610,6 +649,30 @@ form.name_reserved=Der Benutzername "%s" ist reserviert.
 form.name_pattern_not_allowed=Das Muster "%s" ist nicht in einem Benutzernamen erlaubt.
 form.name_chars_not_allowed=Benutzername "%s" enthält ungültige Zeichen.
 
+block.block=Sperren
+block.block.user=Benutzer sperren
+block.block.org=Benutzer für Organisation sperren
+block.block.failure=Fehler beim Sperren des Benutzers: %s
+block.unblock=Entsperren
+block.unblock.failure=Fehler beim Entsperren des Benutzers: %s
+block.blocked=Du hast diesen Benutzer gesperrt.
+block.title=Einen Benutzer sperren
+block.info=Das Blockieren eines Benutzers hindert ihn daran, mit Repositories zu interagieren, wie zum Beispiel das Öffnen oder Kommentieren von Pull Requests oder Issues. Erfahre mehr über das Blockieren eines Benutzers.
+block.info_1=Das Blockieren eines Benutzers verhindert folgende Aktionen auf deinem Konto und deinen Repositories:
+block.info_2=deinem Konto folgen
+block.info_3=dir Benachrichtigungen durch @Erwähnung deines Benutzernamens senden
+block.info_4=dich als Mitarbeiter in deren Repositories einladen
+block.info_5=Repositories favorisieren, forken oder beobachten
+block.info_6=Issues oder Pull Requests öffnen und kommentieren
+block.info_7=auf deine Kommentare in Issues oder Pull Requests reagieren
+block.user_to_block=Zu sperrender Benutzer
+block.note=Anmerkung
+block.note.title=Optionale Anmerkung:
+block.note.info=Die Anmerkung ist für den blockierten Benutzer nicht sichtbar.
+block.note.edit=Anmerkung bearbeiten
+block.list=Gesperrte Benutzer
+block.list.none=Du hast noch keine Benutzer gesperrt.
+
 [settings]
 profile=Profil
 account=Account
@@ -754,7 +817,6 @@ gpg_invalid_token_signature=Der GPG Key, die Signatur, und das Token stimmen nic
 gpg_token_required=Du musst eine Signatur für das folgende Token angeben
 gpg_token=Token
 gpg_token_help=Du kannst eine Signatur wie folgt generieren:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=GPG Textsignatur (armored signature)
 key_signature_gpg_placeholder=Beginnt mit '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=GPG-Schlüssel "%s" wurde verifiziert.
@@ -862,6 +924,7 @@ revoke_oauth2_grant_description=Wenn du die Autorisierung widerrufst, kann die A
 revoke_oauth2_grant_success=Zugriff erfolgreich widerrufen.
 
 twofa_desc=Zwei-Faktor-Authentifizierung trägt zu einer höheren Accountsicherheit bei.
+twofa_recovery_tip=Wenn du dein Gerät verlierst, kannst du einen einmalig verwendbaren Wiederherstellungsschlüssel nutzen, um den Zugriff auf dein Konto wiederherzustellen.
 twofa_is_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung <strong>eingeschaltet</strong>.
 twofa_not_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung momentan nicht eingeschaltet.
 twofa_disable=Zwei-Faktor-Authentifizierung deaktivieren
@@ -884,6 +947,8 @@ webauthn_register_key=Sicherheitsschlüssel hinzufügen
 webauthn_nickname=Nickname
 webauthn_delete_key=Sicherheitsschlüssel entfernen
 webauthn_delete_key_desc=Wenn du einen Sicherheitsschlüssel entfernst, kannst du dich nicht mehr mit ihm anmelden. Fortfahren?
+webauthn_key_loss_warning=Wenn du deine Sicherheitsschlüssel verlierst, verlierst du den Zugriff auf dein Konto.
+webauthn_alternative_tip=Möglicherweise möchtest du eine zusätzliche Authentifizierungsmethode konfigurieren.
 
 manage_account_links=Verknüpfte Accounts verwalten
 manage_account_links_desc=Diese externen Accounts sind mit deinem Gitea-Account verknüpft.
@@ -920,6 +985,7 @@ visibility.private=Privat
 visibility.private_tooltip=Sichtbar nur für Mitglieder von Organisationen, denen du beigetreten bist
 
 [repo]
+new_repo_helper=Ein Repository enthält alle Projektdateien, einschließlich des Änderungsverlaufs. Schon woanders vorhanden? <a href="%s">Migration eines Repositorys.</a>
 owner=Besitzer
 owner_helper=Einige Organisationen könnten in der Dropdown-Liste nicht angezeigt werden, da die Anzahl an Repositories begrenzt ist.
 repo_name=Repository-Name
@@ -943,8 +1009,9 @@ fork_visibility_helper=Die Sichtbarkeit eines geforkten Repositories kann nicht
 fork_branch=Branch, der zum Fork geklont werden soll
 all_branches=Alle Branches
 fork_no_valid_owners=Dieses Repository kann nicht geforkt werden, da keine gültigen Besitzer vorhanden sind.
+fork.blocked_user=Das Repository kann nicht geforkt werden, da du vom Repository-Eigentümer blockiert wurdest.
 use_template=Dieses Template verwenden
-clone_in_vsc=In VS Code klonen
+open_with_editor=Mit %s öffnen
 download_zip=ZIP herunterladen
 download_tar=TAR.GZ herunterladen
 download_bundle=BUNDLE herunterladen
@@ -960,6 +1027,8 @@ issue_labels_helper=Wähle ein Issue-Label-Set.
 license=Lizenz
 license_helper=Wähle eine Lizenz aus.
 license_helper_desc=Eine Lizenz regelt, was Andere mit deinem Code (nicht) tun können. Unsicher, welches für dein Projekt die Richtige ist? Siehe <a target="_blank" rel="noopener noreferrer" href="%s">eine Lizenz wählen</a>.
+object_format=Objektformat
+object_format_helper=Objektformat des Repositories. Es kann später nicht geändert werden. SHA1 ist am meisten kompatibel.
 readme=README
 readme_helper=Wähle eine README-Vorlage aus.
 readme_helper_desc=Hier kannst du eine komplette Beschreibung für dein Projekt schreiben.
@@ -977,6 +1046,7 @@ mirror_prune=Entfernen
 mirror_prune_desc=Entferne veraltete remote-tracking Referenzen
 mirror_interval=Mirror-Intervall (gültige Zeiteinheiten sind 'h', 'm', 's'). 0 deaktiviert die regelmäßige Synchronisation. (Minimales Intervall: %s)
 mirror_interval_invalid=Das Spiegel-Intervall ist ungültig.
+mirror_sync=synchronisiert
 mirror_sync_on_commit=Synchronisieren, wenn Commits gepusht wurden
 mirror_address=Klonen via URL
 mirror_address_desc=Gib alle erforderlichen Anmeldedaten im Abschnitt "Authentifizierung" ein.
@@ -994,6 +1064,7 @@ watchers=Beobachter
 stargazers=Favorisiert von
 stars_remove_warning=Dies wird alle Sterne aus diesem Repository entfernen.
 forks=Forks
+stars=Favoriten
 reactions_more=und %d weitere
 unit_disabled=Der Administrator hat diesen Repository-Bereich deaktiviert.
 language_other=Andere
@@ -1027,6 +1098,7 @@ desc.public=Öffentlich
 desc.template=Template
 desc.internal=Intern
 desc.archived=Archiviert
+desc.sha256=SHA256
 
 template.items=Template-Elemente
 template.git_content=Git Inhalt (Standardbranch)
@@ -1114,6 +1186,7 @@ watch=Beobachten
 unstar=Favorit entfernen
 star=Favorisieren
 fork=Fork
+action.blocked_user=Die Aktion kann nicht ausgeführt werden, da du vom Repository-Eigentümer blockiert wurdest.
 download_archive=Repository herunterladen
 more_operations=Weitere Operationen
 
@@ -1177,6 +1250,8 @@ audio_not_supported_in_browser=Dein Browser unterstützt den HTML5 'audio'-Tag n
 stored_lfs=Gespeichert mit Git LFS
 symbolic_link=Softlink
 executable_file=Ausführbare Datei
+vendored=Vendor
+generated=Generiert
 commit_graph=Commit graph
 commit_graph.select=Branches auswählen
 commit_graph.hide_pr_refs=Pull-Requests ausblenden
@@ -1240,6 +1315,8 @@ editor.file_editing_no_longer_exists=Die bearbeitete Datei "%s" existiert nicht
 editor.file_deleting_no_longer_exists=Die zu löschende Datei "%s" existiert nicht mehr in diesem Repository.
 editor.file_changed_while_editing=Der Inhalt der Datei hat sich seit dem Beginn der Bearbeitung geändert. <a target="_blank" rel="noopener noreferrer" href="%s">Hier klicken</a>, um die Änderungen anzusehen, oder <strong>Änderungen erneut comitten</strong>, um sie zu überschreiben.
 editor.file_already_exists=Eine Datei mit dem Namen '%s' existiert bereits in diesem Repository.
+editor.commit_id_not_matching=Die Commit-ID stimmt nicht mit der ID überein, bei welcher du mit der Bearbeitung begonnen hast. Commite in einen Patch-Branch und merge daraufhin.
+editor.push_out_of_date=Der Push scheint veraltet zu sein.
 editor.commit_empty_file_header=Leere Datei committen
 editor.commit_empty_file_text=Die Datei, die du commiten willst, ist leer. Fortfahren?
 editor.no_changes_to_show=Keine Änderungen vorhanden.
@@ -1263,9 +1340,8 @@ commits.desc=Durchsuche die Quellcode-Änderungshistorie.
 commits.commits=Commits
 commits.no_commits=Keine gemeinsamen Commits. "%s" und "%s" haben vollständig unterschiedliche Historien.
 commits.nothing_to_compare=Diese Branches sind auf demselben Stand.
-commits.search=Commits durchsuchen…
 commits.search.tooltip=Du kannst Suchbegriffen "author:", " committer:", "after:", oder " before:" voranstellen, z.B. "revert author:Alice before:2019-04-01".
-commits.find=Suchen
+commits.search_branch=Dieser Branch
 commits.search_all=Alle Branches
 commits.author=Autor
 commits.message=Nachricht
@@ -1316,7 +1392,6 @@ projects.type.basic_kanban=Einfaches Kanban
 projects.type.bug_triage=Bug Triage
 projects.template.desc=Projektvorlage
 projects.template.desc_helper=Wähle eine Projektvorlage aus, um loszulegen
-projects.type.uncategorized=Nicht kategorisiert
 projects.column.edit=Spalte bearbeiten
 projects.column.edit_title=Name
 projects.column.new_title=Name
@@ -1324,10 +1399,8 @@ projects.column.new_submit=Spalte erstellen
 projects.column.new=Neue Spalte
 projects.column.set_default=Als Standard verwenden
 projects.column.set_default_desc=Diese Spalte als Standard für unkategorisierte Issues und Pull Requests festlegen
-projects.column.unset_default=Standard entfernen
-projects.column.unset_default_desc=Diese Spalte als Standard entfernen
 projects.column.delete=Spalte löschen
-projects.column.deletion_desc=Beim Löschen einer Projektspalte werden alle dazugehörigen Issues nach 'Nicht kategorisiert' verschoben. Fortfahren?
+projects.column.deletion_desc=Beim Löschen einer Projektspalte werden alle Einträge in die Standard-Spalte verschoben. Fortfahren?
 projects.column.color=Farbe
 projects.open=Öffnen
 projects.close=Schließen
@@ -1362,6 +1435,8 @@ issues.new.assignees=Zuständig
 issues.new.clear_assignees=Zuständige entfernen
 issues.new.no_assignees=Niemand zuständig
 issues.new.no_reviewers=Keine Reviewer
+issues.new.blocked_user=Das Issue kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest.
+issues.edit.blocked_user=Der Inhalt kann nicht bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest.
 issues.choose.get_started=Los geht's
 issues.choose.open_external_link=Öffnen
 issues.choose.blank=Standard
@@ -1439,7 +1514,6 @@ issues.filter_sort.moststars=Meiste Favoriten
 issues.filter_sort.feweststars=Wenigste Favoriten
 issues.filter_sort.mostforks=Meiste Forks
 issues.filter_sort.fewestforks=Wenigste Forks
-issues.keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
 issues.action_open=Öffnen
 issues.action_close=Schließen
 issues.action_label=Label
@@ -1477,6 +1551,7 @@ issues.close_comment_issue=Kommentieren und schließen
 issues.reopen_issue=Wieder öffnen
 issues.reopen_comment_issue=Kommentieren und wieder öffnen
 issues.create_comment=Kommentieren
+issues.comment.blocked_user=Der Kommentar kann nicht erstellt oder bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest.
 issues.closed_at=`hat diesen Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen`
 issues.reopened_at=`hat diesen Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> wieder geöffnet`
 issues.commit_ref_at=`hat dieses Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> aus einem Commit referenziert`
@@ -1675,6 +1750,7 @@ compare.compare_head=vergleichen
 
 pulls.desc=Pull-Requests und Code-Reviews aktivieren.
 pulls.new=Neuer Pull-Request
+pulls.new.blocked_user=Der Pull Request kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest.
 pulls.view=Pull-Request ansehen
 pulls.compare_changes=Neuer Pull-Request
 pulls.allow_edits_from_maintainers=Änderungen von Maintainern erlauben
@@ -1691,7 +1767,6 @@ pulls.compare_compare=pullen von
 pulls.switch_comparison_type=Vergleichstyp wechseln
 pulls.switch_head_and_base=Head und Base vertauschen
 pulls.filter_branch=Branch filtern
-pulls.no_results=Keine Ergebnisse verfügbar.
 pulls.show_all_commits=Alle Commits anzeigen
 pulls.show_changes_since_your_last_review=Zeige Änderungen seit deinem letzten Review
 pulls.showing_only_single_commit=Nur Änderungen aus Commit %[1]s werden angezeigt
@@ -1700,6 +1775,7 @@ pulls.select_commit_hold_shift_for_range=Commit auswählen. Halte Shift + klicke
 pulls.review_only_possible_for_full_diff=Ein Review ist nur möglich, wenn das vollständige Diff angezeigt wird
 pulls.filter_changes_by_commit=Nach Commit filtern
 pulls.nothing_to_compare=Diese Branches sind identisch. Es muss kein Pull-Request erstellt werden.
+pulls.nothing_to_compare_have_tag=Der ausgewählte Branch und Tag sind gleich.
 pulls.nothing_to_compare_and_allow_empty_pr=Diese Branches sind gleich. Der Pull-Request wird leer sein.
 pulls.has_pull_request=`Es existiert bereits ein Pull-Request zwischen diesen beiden Branches: <a href="%[1]s">%[2]s#%[3]d</a>`
 pulls.create=Pull-Request erstellen
@@ -1758,6 +1834,7 @@ pulls.merge_pull_request=Merge Commit erstellen
 pulls.rebase_merge_pull_request=Rebasen und dann fast-forwarden
 pulls.rebase_merge_commit_pull_request=Rebasen und dann mergen
 pulls.squash_merge_pull_request=Squash Commit erstellen
+pulls.fast_forward_only_merge_pull_request=Nur Fast-forward
 pulls.merge_manually=Manuell mergen
 pulls.merge_commit_id=Der Mergecommit ID
 pulls.require_signed_wont_sign=Der Branch erfordert einen signierten Commit, aber dieser Merge wird nicht signiert
@@ -1782,6 +1859,8 @@ pulls.status_checks_failure=Einige Prüfungen sind fehlgeschlagen
 pulls.status_checks_error=Einige Checks meldeten Fehler
 pulls.status_checks_requested=Erforderlich
 pulls.status_checks_details=Details
+pulls.status_checks_hide_all=Alle Prüfungen ausblenden
+pulls.status_checks_show_all=Alle Prüfungen anzeigen
 pulls.update_branch=Branch durch Mergen aktualisieren
 pulls.update_branch_rebase=Branch durch Rebase aktualisieren
 pulls.update_branch_success=Branch-Aktualisierung erfolgreich
@@ -1790,6 +1869,11 @@ pulls.outdated_with_base_branch=Dieser Branch enthält nicht die neusten Commits
 pulls.close=Pull-Request schließen
 pulls.closed_at=`hat diesen Pull-Request <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen`
 pulls.reopened_at=`hat diesen Pull-Request <a id="%[1]s" href="#%[1]s">%[2]s</a> wieder geöffnet`
+pulls.cmd_instruction_hint=`Zeige <a class="show-instruction">Kommandozeilenanweisungen</a>.`
+pulls.cmd_instruction_checkout_title=Checkout
+pulls.cmd_instruction_checkout_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen.
+pulls.cmd_instruction_merge_title=Mergen
+pulls.cmd_instruction_merge_desc=Die Änderungen mergen und auf Gitea aktualisieren.
 pulls.clear_merge_message=Merge-Nachricht löschen
 pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie "Co-Authored-By …" erhalten.
 
@@ -1887,6 +1971,10 @@ wiki.page_name_desc=Gib einen Namen für diese Wiki-Seite ein. Spezielle Namen s
 wiki.original_git_entry_tooltip=Originale Git-Datei anstatt eines benutzerfreundlichen Links anzeigen.
 
 activity=Aktivität
+activity.navbar.pulse=Puls
+activity.navbar.code_frequency=Code-Frequenz
+activity.navbar.contributors=Mitwirkende
+activity.navbar.recent_commits=Neueste Commits
 activity.period.filter_label=Zeitraum:
 activity.period.daily=1 Tag
 activity.period.halfweekly=3 Tage
@@ -1952,16 +2040,10 @@ activity.git_stats_and_deletions=und
 activity.git_stats_deletion_1=%d Löschung
 activity.git_stats_deletion_n=%d Löschungen
 
-search=Suchen
-search.search_repo=Repository durchsuchen
-search.type.tooltip=Suchmodus
-search.fuzzy=Ähnlich
-search.fuzzy.tooltip=Zeige auch Ergebnisse, die dem Suchbegriff ähneln
-search.match=Genau
-search.match.tooltip=Zeige nur Ergebnisse, die exakt mit dem Suchbegriff übereinstimmen
-search.results=Suchergebnisse für „%s“ in <a href="%s"> %s</a>
-search.code_no_results=Es konnte kein passender Code für deinen Suchbegriff gefunden werden.
-search.code_search_unavailable=Derzeit ist die Code-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
+contributors.contribution_type.filter_label=Beitragstyp:
+contributors.contribution_type.commits=Commits
+contributors.contribution_type.additions=Ergänzungen
+contributors.contribution_type.deletions=Löschungen
 
 settings=Einstellungen
 settings.desc=In den Einstellungen kannst du die Einstellungen des Repositories anpassen
@@ -1989,6 +2071,7 @@ settings.mirror_settings.docs.doc_link_title=Wie spiegele ich Repositories?
 settings.mirror_settings.docs.doc_link_pull_section=den Abschnitt "Von einem entfernten Repository pullen" in der Dokumentation.
 settings.mirror_settings.docs.pulling_remote_title=Aus einem Remote-Repository pullen
 settings.mirror_settings.mirrored_repository=Gespiegeltes Repository
+settings.mirror_settings.pushed_repository=Gepushtes Repository
 settings.mirror_settings.direction=Richtung
 settings.mirror_settings.direction.pull=Pull
 settings.mirror_settings.direction.push=Push
@@ -2010,6 +2093,8 @@ settings.branches.add_new_rule=Neue Regel hinzufügen
 settings.advanced_settings=Erweiterte Einstellungen
 settings.wiki_desc=Repository-Wiki aktivieren
 settings.use_internal_wiki=Eingebautes Wiki verwenden
+settings.default_wiki_branch_name=Standardbezeichnung für Wiki-Branch
+settings.failed_to_change_default_wiki_branch=Das Ändern des Standard-Wiki-Branches ist fehlgeschlagen.
 settings.use_external_wiki=Externes Wiki verwenden
 settings.external_wiki_url=Externe Wiki-URL
 settings.external_wiki_url_error=Die externe Wiki-URL ist ungültig.
@@ -2040,6 +2125,10 @@ settings.pulls.default_allow_edits_from_maintainers=Änderungen von Maintainern
 settings.releases_desc=Repository-Releases aktivieren
 settings.packages_desc=Repository Packages Registry aktivieren
 settings.projects_desc=Repository-Projekte aktivieren
+settings.projects_mode_desc=Projekte-Modus (welche Art Projekte angezeigt werden sollen)
+settings.projects_mode_repo=Nur Repo-Projekte
+settings.projects_mode_owner=Nur Benutzer- oder Organisations-Projekte
+settings.projects_mode_all=Alle Projekte
 settings.actions_desc=Repository-Actions aktivieren
 settings.admin_settings=Administratoreinstellungen
 settings.admin_enable_health_check=Repository-Health-Checks aktivieren (git fsck)
@@ -2065,6 +2154,7 @@ settings.convert_fork_succeed=Der Fork wurde in ein normales Repository konverti
 settings.transfer=Besitz übertragen
 settings.transfer.rejected=Repository-Übertragung wurde abgelehnt.
 settings.transfer.success=Repository-Übertragung war erfolgreich.
+settings.transfer.blocked_user=Das Repository kann nicht übertragen werden, da du vom Repository-Eigentümer blockiert wurdest.
 settings.transfer_abort=Übertragung abbrechen
 settings.transfer_abort_invalid=Du kannst nur eingeleitete Repository-Übertragung abbrechen.
 settings.transfer_abort_success=Die Repository-Übertragung zu %s wurde abgebrochen.
@@ -2110,11 +2200,11 @@ settings.add_collaborator_success=Der Mitarbeiter wurde hinzugefügt.
 settings.add_collaborator_inactive_user=Inaktive Benutzer können nicht als Mitarbeiter hinzufügt werden.
 settings.add_collaborator_owner=Besitzer können nicht als Mitarbeiter hinzugefügt werden.
 settings.add_collaborator_duplicate=Der Mitarbeiter ist bereits zu diesem Repository hinzugefügt.
+settings.add_collaborator.blocked_user=Der Mitwirkende wurde vom Eigentümer des Repositories blockiert oder umgekehrt.
 settings.delete_collaborator=Entfernen
 settings.collaborator_deletion=Mitarbeiter entfernen
 settings.collaborator_deletion_desc=Nach dem Löschen wird dieser Mitarbeiter keinen Zugriff mehr auf dieses Repository haben. Fortfahren?
 settings.remove_collaborator_success=Der Mitarbeiter wurde entfernt.
-settings.search_user_placeholder=Benutzer suchen…
 settings.org_not_allowed_to_be_collaborator=Organisationen können nicht als Mitarbeiter hinzugefügt werden.
 settings.change_team_access_not_allowed=Nur der Besitzer der Organisation kann die Zugangsrechte des Teams ändern
 settings.team_not_in_organization=Das Team ist nicht in der gleichen Organisation wie das Repository
@@ -2122,7 +2212,6 @@ settings.teams=Teams
 settings.add_team=Team hinzufügen
 settings.add_team_duplicate=Das Team ist dem Repository schon zugeordnet
 settings.add_team_success=Das Team hat nun Zugriff auf das Repository.
-settings.search_team=Team suchen…
 settings.change_team_permission_tip=Die Team-Berechtigung ist auf der Team-Einstellungsseite festgelegt und kann nicht für ein Repository geändert werden
 settings.delete_team_tip=Dieses Team hat Zugriff auf alle Repositories und kann nicht entfernt werden
 settings.remove_team_success=Der Zugriff des Teams auf das Repository wurde zurückgezogen.
@@ -2275,9 +2364,7 @@ settings.protect_whitelist_committers=Schütze gewhitelistete Commiter
 settings.protect_whitelist_committers_desc=Jeder, der auf der Whitelist steht, darf in diesen Branch pushen (aber kein Force-Push).
 settings.protect_whitelist_deploy_keys=Deploy-Schlüssel mit Schreibzugriff zum Pushen whitelisten.
 settings.protect_whitelist_users=Nutzer, die pushen dürfen:
-settings.protect_whitelist_search_users=Benutzer suchen…
 settings.protect_whitelist_teams=Teams, die pushen dürfen:
-settings.protect_whitelist_search_teams=Teams suchen…
 settings.protect_merge_whitelist_committers=Merge-Whitelist aktivieren
 settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Whitelist Pull-Requests in diesen Branch zu mergen.
 settings.protect_merge_whitelist_users=Nutzer, die mergen dürfen:
@@ -2298,9 +2385,12 @@ settings.protect_approvals_whitelist_users=Freigeschaltete Reviewer:
 settings.protect_approvals_whitelist_teams=Freigeschaltete Teams:
 settings.dismiss_stale_approvals=Entferne alte Genehmigungen
 settings.dismiss_stale_approvals_desc=Wenn neue Commits gepusht werden, die den Inhalt des Pull-Requests ändern, werden alte Genehmigungen entfernt.
+settings.ignore_stale_approvals=Veraltete Genehmigungen ignorieren
+settings.ignore_stale_approvals_desc=Genehmigungen, die für ältere Commits erteilt wurden (veraltete Genehmigungen), nicht bei der Anzahl an Genehmigungen mitzählen. Irrelevant, falls veraltete Genehmigungen bereits verworfen wurden.
 settings.require_signed_commits=Signierte Commits erforderlich
 settings.require_signed_commits_desc=Pushes auf diesen Branch ablehnen, wenn Commits nicht signiert oder nicht überprüfbar sind.
 settings.protect_branch_name_pattern=Muster für geschützte Branchnamen
+settings.protect_branch_name_pattern_desc=Geschützte Branch-Namensmuster. Siehe <a href="https://github.com/gobwas/glob">die Dokumentation</a> für die Muster-Syntax. Beispiele: main, release/**
 settings.protect_patterns=Muster
 settings.protect_protected_file_patterns=Geschützte Dateimuster (durch Semikolon ';' getrennt):
 settings.protect_protected_file_patterns_desc=Geschützte Dateien dürfen nicht direkt geändert werden, auch wenn der Benutzer Rechte hat, Dateien in diesem Branch hinzuzufügen, zu bearbeiten oder zu löschen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> Dokumentation zur Mustersyntax. Beispiele: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
@@ -2352,6 +2442,7 @@ settings.archive.error=Beim Versuch, das Repository zu archivieren, ist ein Fehl
 settings.archive.error_ismirror=Du kannst keinen Repo-Mirror archivieren.
 settings.archive.branchsettings_unavailable=Branch-Einstellungen sind nicht verfügbar wenn das Repo archiviert ist.
 settings.archive.tagsettings_unavailable=Tag Einstellungen sind nicht verfügbar, wenn das Repo archiviert wurde.
+settings.archive.mirrors_unavailable=Mirrors sind nicht verfügbar, wenn das Repository archiviert ist.
 settings.unarchive.button=Archivieren rückgängig machen
 settings.unarchive.header=Archivieren dieses Repositories rückgängig machen
 settings.unarchive.text=Durch das Aufheben der Archivierung kann das Repo wieder Commits und Pushes sowie neue Issues und Pull-Requests empfangen.
@@ -2518,7 +2609,6 @@ branch.default_deletion_failed=Branch "%s" kann nicht gelöscht werden, da diese
 branch.restore=Branch "%s" wiederherstellen
 branch.download=Branch "%s" herunterladen
 branch.rename=Branch "%s" umbenennen
-branch.search=Branch suchen
 branch.included_desc=Dieser Branch ist im Standard-Branch enthalten
 branch.included=Enthalten
 branch.create_new_branch=Branch aus Branch erstellen:
@@ -2549,6 +2639,16 @@ find_file.no_matching=Keine passende Datei gefunden
 error.csv.too_large=Diese Datei kann nicht gerendert werden, da sie zu groß ist.
 error.csv.unexpected=Diese Datei kann nicht gerendert werden, da sie ein unerwartetes Zeichen in Zeile %d und Spalte %d enthält.
 error.csv.invalid_field_count=Diese Datei kann nicht gerendert werden, da sie eine falsche Anzahl an Feldern in Zeile %d hat.
+error.broken_git_hook=Git-Hooks dieses Repositories scheinen defekt zu sein. Bitte folge der <a target="_blank" rel="noreferrer" href="%s">Dokumentation</a>, um dies zu beheben, pushe dann ein paar Commits und den Status zu aktualisieren.
+
+[graphs]
+component_loading=%s werden geladen ...
+component_loading_failed=%s konnten nicht geladen werden
+component_loading_info=Dies kann ein wenig dauern …
+component_failed_to_load=Ein unerwarteter Fehler ist aufgetreten.
+code_frequency.what=Code-Frequenz
+contributors.what=Beiträge
+recent_commits.what=Neueste Commits
 
 [org]
 org_name_holder=Name der Organisation
@@ -2654,7 +2754,6 @@ teams.write_permission_desc=Dieses Team hat <strong>Schreibzugriff</strong>: Mit
 teams.admin_permission_desc=Dieses Team hat <strong>Adminzugriff</strong>: Mitglieder dieses Teams können Team-Repositories ansehen, auf sie pushen und Mitarbeiter hinzufügen.
 teams.create_repo_permission_desc=Zusätzlich erteilt dieses Team die Berechtigung <strong>Repository erstellen</strong>: Mitglieder können neue Repositories in der Organisation erstellen.
 teams.repositories=Team-Repositories
-teams.search_repo_placeholder=Repository durchsuchen…
 teams.remove_all_repos_title=Alle Team-Repositories entfernen
 teams.remove_all_repos_desc=Dies entfernt alle Repositories von dem Team.
 teams.add_all_repos_title=Alle Repositories hinzufügen
@@ -2663,6 +2762,7 @@ teams.add_nonexistent_repo=Das Repository, das du hinzufügen möchtest, existie
 teams.add_duplicate_users=Dieser Benutzer ist bereits ein Teammitglied.
 teams.repos.none=Dieses Team hat Zugang zu keinem Repository.
 teams.members.none=Keine Mitglieder in diesem Team.
+teams.members.blocked_user=Der Benutzer kann nicht hinzugefügt werden, da er von der Organisation blockiert wurde.
 teams.specific_repositories=Bestimmte Repositories
 teams.specific_repositories_helper=Mitglieder haben nur Zugriff auf Repositories, die explizit dem Team hinzugefügt wurden. Wenn Du diese Option wählst, werden Repositories, die bereits mit <i>Alle Repositories</i> hinzugefügt wurden, <strong>nicht</strong> automatisch entfernt.
 teams.all_repositories=Alle Repositories
@@ -2676,6 +2776,7 @@ teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Tea
 
 [admin]
 dashboard=Dashboard
+self_check=Selbstprüfung
 identity_access=Identität & Zugriff
 users=Benutzerkonten
 organizations=Organisationen
@@ -2686,6 +2787,8 @@ integrations=Integrationen
 authentication=Authentifizierungsquellen
 emails=Benutzer E-Mails
 config=Konfiguration
+config_summary=Übersicht
+config_settings=Einstellungen
 notices=Systemmitteilungen
 monitor=Monitoring
 first_page=Erste
@@ -2695,7 +2798,6 @@ settings=Administratoreinstellungen
 
 dashboard.new_version_hint=Gitea %s ist jetzt verfügbar, deine derzeitige Version ist %s. Weitere Details findest du im <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">Blog</a>.
 dashboard.statistic=Übersicht
-dashboard.operations=Wartungsoperationen
 dashboard.system_status=System-Status
 dashboard.operation_name=Name der Operation
 dashboard.operation_switch=Wechseln
@@ -2721,6 +2823,7 @@ dashboard.delete_missing_repos=Alle Repository-Datensätze mit verloren gegangen
 dashboard.delete_missing_repos.started=Alle Repositories löschen, die den Git-File-Task nicht gestartet haben.
 dashboard.delete_generated_repository_avatars=Generierte Repository-Avatare löschen
 dashboard.sync_repo_branches=Fehlende Branches aus den Git-Daten in die Datenbank synchronisieren
+dashboard.sync_repo_tags=Tags von Git-Daten in die Datenbank synchronisieren
 dashboard.update_mirrors=Mirrors aktualisieren
 dashboard.repo_health_check=Healthchecks für alle Repositories ausführen
 dashboard.check_repo_stats=Überprüfe alle Repository-Statistiken
@@ -2775,6 +2878,7 @@ dashboard.stop_endless_tasks=Endlose Aufgaben stoppen
 dashboard.cancel_abandoned_jobs=Aufgegebene Jobs abbrechen
 dashboard.start_schedule_tasks=Terminierte Aufgaben starten
 dashboard.sync_branch.started=Synchronisierung der Branches gestartet
+dashboard.sync_tag.started=Tag-Synchronisierung gestartet
 dashboard.rebuild_issue_indexer=Issue-Indexer neu bauen
 
 users.user_manage_panel=Benutzerkontenverwaltung
@@ -2846,6 +2950,7 @@ emails.updated=E-Mail aktualisiert
 emails.not_updated=Fehler beim Aktualisieren der angeforderten E-Mail-Adresse: %v
 emails.duplicate_active=Diese E-Mail-Adresse wird bereits von einem Nutzer verwendet.
 emails.change_email_header=E-Mail-Eigenschaften aktualisieren
+emails.change_email_text=Bist du dir sicher, dass du diese E-Mail-Adresse aktualisieren möchtest?
 
 orgs.org_manage_panel=Organisationsverwaltung
 orgs.name=Name
@@ -2859,9 +2964,6 @@ repos.unadopted.no_more=Keine weiteren nicht übernommenen Repositories gefunden
 repos.owner=Besitzer
 repos.name=Name
 repos.private=Privat
-repos.watches=Beobachtungen
-repos.stars=Favoriten
-repos.forks=Forks
 repos.issues=Issues
 repos.size=Größe
 repos.lfs_size=LFS-Größe
@@ -2870,6 +2972,7 @@ packages.package_manage_panel=Paketverwaltung
 packages.total_size=Gesamtgröße: %s
 packages.unreferenced_size=Nicht referenzierte Größe: %s
 packages.cleanup=Veraltete Daten löschen
+packages.cleanup.success=Abgelaufene Daten erfolgreich bereinigt
 packages.owner=Besitzer
 packages.creator=Ersteller
 packages.name=Name
@@ -2985,7 +3088,7 @@ auths.tip.nextcloud=Registriere über das "Settings -> Security -> OAuth 2.0 cli
 auths.tip.dropbox=Erstelle eine neue App auf https://www.dropbox.com/developers/apps.
 auths.tip.facebook=Erstelle eine neue Anwendung auf https://developers.facebook.com/apps und füge das Produkt „Facebook Login“ hinzu.
 auths.tip.github=Erstelle unter https://github.com/settings/applications/new eine neue OAuth-Anwendung.
-auths.tip.gitlab=Erstelle unter https://gitlab.com/profile/applications eine neue Anwendung.
+auths.tip.gitlab_new=Erstelle eine neue Anwendung unter https://gitlab.com/-/profile/applications
 auths.tip.google_plus=Du erhältst die OAuth2-Client-Zugangsdaten in der Google-API-Konsole unter https://console.developers.google.com/
 auths.tip.openid_connect=Benutze die OpenID-Connect-Discovery-URL (<server>/.well-known/openid-configuration), um die Endpunkte zu spezifizieren
 auths.tip.twitter=Gehe auf https://dev.twitter.com/apps, erstelle eine Anwendung und stelle sicher, dass die Option „Allow this application to be used to Sign in with Twitter“ aktiviert ist
@@ -3121,6 +3224,7 @@ config.picture_config=Bild-und-Profilbild-Konfiguration
 config.picture_service=Bilderservice
 config.disable_gravatar=Gravatar deaktivieren
 config.enable_federated_avatar=Föderierte Profilbilder einschalten
+config.open_with_editor_app_help=Die „Öffnen mit“-Editoren für das Klon-Menü. Falls leer, wird die Standardeinstellung verwendet. Erweitern, um die Standardeinstellung zu sehen.
 
 config.git_config=Git-Konfiguration
 config.git_disable_diff_highlight=Diff-Syntaxhervorhebung ausschalten
@@ -3199,6 +3303,13 @@ notices.desc=Beschreibung
 notices.op=Aktion
 notices.delete_success=Diese Systemmeldung wurde gelöscht.
 
+self_check.no_problem_found=Bisher wurde kein Problem festgestellt.
+self_check.database_collation_mismatch=Erwarte Datenbank-Kollation: %s
+self_check.database_collation_case_insensitive=Die Datenbank verwendet die Kollation %s, was eine unsensible Kollation ist. Obwohl Gitea damit arbeiten könnte, gibt es vielleicht einige seltene Fälle, die nicht wie erwartet funktionieren.
+self_check.database_inconsistent_collation_columns=Die Datenbank verwendet die Kollation %s, aber diese Spalten verwenden unzutreffende Kollationen. Dies könnte zu unerwarteten Problemen führen.
+self_check.database_fix_mysql=Für MySQL/MariaDB-Benutzer kann man den Befehl "gitea doctor convert" oder manuell auch "ALTER ... COLLATE ..."-SQLs verwenden, um die Sortierprobleme zu beheben.
+self_check.database_fix_mssql=Für MSSQL-Benutzer kann das Problem im Moment nur durch "ALTER ... COLLATE ..." SQLs manuell behoben werden.
+
 [action]
 create_repo=hat das Repository <a href="%s">%s</a> erstellt
 rename_repo=hat das Repository von <code>%[1]s</code> zu <a href="%[2]s">%[3]s</a> umbenannt
@@ -3383,6 +3494,9 @@ rpm.registry=Diese Registry über die Kommandozeile einrichten:
 rpm.distros.redhat=auf RedHat-basierten Distributionen
 rpm.distros.suse=auf SUSE-basierten Distributionen
 rpm.install=Nutze folgenden Befehl, um das Paket zu installieren:
+rpm.repository=Repository-Informationen
+rpm.repository.architectures=Architekturen
+rpm.repository.multiple_groups=Dieses Paket ist in mehreren Gruppen verfügbar.
 rubygems.install=Um das Paket mit gem zu installieren, führe den folgenden Befehl aus:
 rubygems.install2=oder füg es zum Gemfile hinzu:
 rubygems.dependencies.runtime=Laufzeitabhängigkeiten
@@ -3508,12 +3622,18 @@ runs.commit=Commit
 runs.scheduled=Geplant
 runs.pushed_by=gepusht von
 runs.invalid_workflow_helper=Die Workflow-Konfigurationsdatei ist ungültig. Bitte überprüfe Deine Konfigurationsdatei: %s
+runs.no_matching_online_runner_helper=Kein passender Runner online mit Label: %s
+runs.no_job_without_needs=Der Workflow muss mindestens einen Job ohne Abhängigkeiten enthalten.
 runs.actor=Initiator
 runs.status=Status
 runs.actors_no_select=Alle Initiatoren
 runs.status_no_select=Alle Status
 runs.no_results=Keine passenden Ergebnisse gefunden.
+runs.no_workflows=Es gibt noch keine Workflows.
+runs.no_workflows.quick_start=Du weißt nicht, wie du mit Gitea Actions loslegst? Siehe <a target="_blank" rel="noopener noreferrer" href="%s">die Schnellstart-Anleitung</a>.
+runs.no_workflows.documentation=Weitere Informationen zu Gitea Actions findest du in der <a target="_blank" rel="noopener noreferrer" href="%s"> Dokumentation</a>.
 runs.no_runs=Der Workflow hat noch keine Ausführungen.
+runs.empty_commit_message=(leere Commit-Nachricht)
 
 workflow.disable=Workflow deaktivieren
 workflow.disable_success=Workflow '%s' erfolgreich deaktiviert.
@@ -3530,7 +3650,7 @@ variables.none=Es gibt noch keine Variablen.
 variables.deletion=Variable entfernen
 variables.deletion.description=Das Entfernen einer Variable ist dauerhaft und kann nicht rückgängig gemacht werden. Fortfahren?
 variables.description=Variablen werden an bestimmte Aktionen übergeben und können nicht anderweitig gelesen werden.
-variables.id_not_exist=Variable mit ID %d existiert nicht.
+variables.id_not_exist=Eine Variable mit ID %d existiert nicht.
 variables.edit=Variable bearbeiten
 variables.deletion.failed=Fehler beim Entfernen der Variable.
 variables.deletion.success=Die Variable wurde entfernt.
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 2424ee3fb6..6ce5ae1ce9 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -4,6 +4,7 @@ explore=Εξερεύνηση
 help=Βοήθεια
 logo=Λογότυπο
 sign_in=Είσοδος
+sign_in_with_provider=Είσοδος με %s
 sign_in_or=ή
 sign_out=Έξοδος
 sign_up=Εγγραφή
@@ -16,6 +17,7 @@ template=Πρότυπο
 language=Γλώσσα
 notifications=Ειδοποιήσεις
 active_stopwatch=Ενεργή Καταγραφή Χρόνου
+tracked_time_summary=Περίληψη του χρόνου παρακολούθησης με βάση τα φίλτρα της λίστας ζητημάτων
 create_new=Δημιουργία…
 user_profile_and_more=Προφίλ και ρυθμίσεις…
 signed_in_as=Είσοδος ως
@@ -79,6 +81,7 @@ milestones=Ορόσημα
 
 ok=OK
 cancel=Ακύρωση
+retry=Επανάληψη
 rerun=Επανεκτέλεση
 rerun_all=Επανεκτέλεση όλων
 save=Αποθήκευση
@@ -88,12 +91,15 @@ remove=Αφαίρεση
 remove_all=Αφαίρεση Όλων
 remove_label_str=`Αφαίρεση του αντικειμένου "%s"`
 edit=Επεξεργασία
+view=Προβολή
 
 enabled=Ενεργοποιημένο
 disabled=Απενεργοποιημένο
+locked=Κλειδωμένο
 
 copy=Αντιγραφή
 copy_url=Αντιγραφή URL
+copy_hash=Αντιγραφή hash
 copy_content=Αντιγραφή περιεχομένου
 copy_branch=Αντιγραφή ονόματος κλάδου
 copy_success=Αντιγράφηκε!
@@ -106,6 +112,7 @@ loading=Φόρτωση…
 
 error=Σφάλμα
 error404=Η σελίδα που προσπαθείτε να φτάσετε είτε <strong>δεν υπάρχει</strong> είτε <strong>δεν είστε εξουσιοδοτημένοι</strong> για να την δείτε.
+go_back=Επιστροφή
 
 never=Ποτέ
 unknown=Άγνωστη
@@ -127,11 +134,22 @@ concept_user_organization=Οργανισμός
 show_timestamps=Εμφάνιση χρονοσημάνσεων
 show_log_seconds=Εμφάνιση δευτερολέπτων
 show_full_screen=Εμφάνιση πλήρους οθόνης
+download_logs=Λήψη καταγραφών
 
+confirm_delete_selected=Επιβεβαιώνετε τη διαγραφή όλων των επιλεγμένων στοιχείων;
 
 name=Όνομα
 value=Τιμή
 
+filter=Φίλτρο
+filter.is_archived=Αρχειοθετήθηκε
+filter.is_template=Πρότυπο
+filter.public=Δημόσιος
+filter.private=Ιδιωτικό
+
+
+[search]
+
 [aria]
 navbar=Γραμμή Πλοήγησης
 footer=Υποσέλιδο
@@ -166,6 +184,7 @@ string.desc=Z - A
 
 [error]
 occurred=Παρουσιάστηκε ένα σφάλμα
+report_message=Αν πιστεύετε ότι αυτό είναι ένα πρόβλημα στο Gitea, παρακαλούμε αναζητήστε ζητήματα στο <a href="https://github.com/go-gitea/gitea/issues" target="_blank">GitHub</a> ή ανοίξτε ένα νέο ζήτημα εάν είναι απαραίτητο.
 missing_csrf=Bad Request: δεν υπάρχει διακριτικό CSRF
 invalid_csrf=Λάθος Αίτημα: μη έγκυρο διακριτικό CSRF
 not_found=Ο προορισμός δεν βρέθηκε.
@@ -174,6 +193,7 @@ network_error=Σφάλμα δικτύου
 [startpage]
 app_desc=Μια ανώδυνη, αυτο-φιλοξενούμενη υπηρεσία Git
 install=Εύκολο στην εγκατάσταση
+install_desc=Απλά <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">εκτελέστε το αρχείο προγράμματος</a> για την πλατφόρμα σας, χρήσιμοποιήστε το με το <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a>, ή εγκαταστήστε το <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">πακέτο</a>.
 platform=Πολυπλατφορμικό
 platform_desc=Ο Gitea τρέχει οπουδήποτε <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> μπορεί να γίνει compile για: Windows, macOS, Linux, ARM, κλπ. Επιλέξτε αυτό που αγαπάτε!
 lightweight=Ελαφρύ
@@ -218,6 +238,7 @@ repo_path_helper=Τα απομακρυσμένα αποθετήρια Git θα 
 lfs_path=Ριζική Διαδρομή Git LFS
 lfs_path_helper=Τα αρχεία που παρακολουθούνται από το Git LFS θα αποθηκεύονται σε αυτόν τον φάκελο. Αφήστε κενό για να το απενεργοποιήσετε.
 run_user=Εκτέλεση Σαν Χρήστη
+run_user_helper=Το όνομα του χρήστη του λειτουργικού συστήματος ο οποίος εκτελεί το Gitea. Επισημαίνεται ότι αυτός ο χρήστης πρέπει να έχει πρόσβαση στο ριζικό φάκελο του αποθετηρίου.
 domain=Domain Διακομιστή
 domain_helper=Όνομα domain διακομιστή ή η διεύθυνση του.
 ssh_port=Θύρα της υπηρεσίας SSH
@@ -290,6 +311,7 @@ password_algorithm_helper=Ορίστε τον αλγόριθμο κατακερ
 enable_update_checker=Ενεργοποίηση Ελεγκτή Ενημερώσεων
 enable_update_checker_helper=Ελέγχει περιοδικά για νέες εκδόσεις κάνοντας σύνδεση στο gitea.io.
 env_config_keys=Ρυθμίσεις Περιβάλλοντος
+env_config_keys_prompt=Οι ακόλουθες μεταβλητές περιβάλλοντος θα εφαρμοστούν επίσης στο αρχείο ρυθμίσεων σας:
 
 [home]
 uname_holder=Όνομα Χρήστη ή Διεύθυνση Email
@@ -301,7 +323,6 @@ collaborative_repos=Συνεργατικά Αποθετήρια
 my_orgs=Οι Οργανισμοί Μου
 my_mirrors=Τα Αντίγραφα Μου
 view_home=Προβολή %s
-search_repos=Βρείτε ένα αποθετήριο…
 filter=Άλλα Φίλτρα
 filter_by_team_repositories=Φιλτράρισμα ανά αποθετήρια ομάδας
 feed_of=`Τροφοδοσία του "%s"`
@@ -322,20 +343,8 @@ issues.in_your_repos=Στα αποθετήρια σας
 repos=Αποθετήρια
 users=Χρήστες
 organizations=Οργανισμοί
-search=Αναζήτηση
 go_to=Μετάβαση σε
 code=Κώδικας
-search.type.tooltip=Τύπος αναζήτησης
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Συμπερίληψη και των αποτελεσμάτων που είναι πλησιέστερα με τον όρο αναζήτησης
-search.match=Ταίριασμα
-search.match.tooltip=Συμπερίληψη μόνο των αποτελεσμάτων που ταιριάζουν ακριβώς με τον όρο αναζήτησης
-code_search_unavailable=Η αναζήτηση κώδικα δεν είναι διαθέσιμη αυτή τη στιγμή. Παρακαλώ επικοινωνήστε με το διαχειριστή.
-repo_no_results=Δεν βρέθηκαν αποθετήρια που να ταιρίαζουν με τα κριτήρια.
-user_no_results=Δεν βρέθηκαν χρήστες που να ταιριάζουν με τα κριτήρια.
-org_no_results=Δεν βρέθηκαν οργανισμοί που να ταιριάζουν με τα κριτήρια.
-code_no_results=Δεν βρέθηκε πηγαίος κώδικας που να ταιριάζει με τον όρο αναζήτησης.
-code_search_results=`Αποτελέσματα αναζήτησης για "%s"`
 code_last_indexed_at=Τελευταίο δημιουργία ευρετηρίου στις %s
 relevant_repositories_tooltip=Τα αποθετήρια που είναι forks ή που δεν έχουν θέμα, εικονίδιο και περιγραφή είναι κρυμμένα.
 relevant_repositories=Εμφανίζονται μόνο τα σχετικά αποθετήρια, <a href="%s">εμφάνιση χωρίς φίλτρο</a>.
@@ -348,16 +357,18 @@ disable_register_prompt=Η εγγραφή είναι απενεργοποιημ
 disable_register_mail=Η Επιβεβαίωση email για την εγγραφή είναι απενεργοποιημένη.
 manual_activation_only=Επικοινωνήστε με το διαχειριστή της υπηρεσίας για να ολοκληρώσετε την ενεργοποίηση.
 remember_me=Απομνημόνευση αυτής της συσκευής
+remember_me.compromised=Το διακριτικό σύνδεσης δεν είναι πλέον έγκυρο, αυτό ίσως υποδεικνύει έναν κλεμμένο λογαριασμό. Παρακαλώ ελέγξτε το λογαριασμό σας για ασυνήθιστες δραστηριότητες.
 forgot_password_title=Ξέχασα Τον Κωδικό Πρόσβασης
 forgot_password=Ξεχάσατε τον κωδικό πρόσβασης;
 sign_up_now=Χρειάζεστε λογαριασμό; Εγγραφείτε τώρα.
-confirmation_mail_sent_prompt=Ένα νέο email επιβεβαίωσης έχει σταλεί στο <b>%s</b>. Παρακαλώ ελέγξτε τα εισερχόμενα σας μέσα στις επόμενες %s για να ολοκληρώσετε τη διαδικασία εγγραφής.
+sign_up_successful=Ο λογαριασμός δημιουργήθηκε επιτυχώς. Καλώς ορίσατε!
 must_change_password=Ενημερώστε τον κωδικό πρόσβασης σας
 allow_password_change=Απαιτείται από το χρήστη να αλλάξει τον κωδικό πρόσβασης (συνιστόμενο)
 reset_password_mail_sent_prompt=Ένα email επιβεβαίωσης έχει σταλεί στο <b>%s</b>. Παρακαλώ ελέγξτε τα εισερχόμενα σας στις επόμενες %s για να ολοκληρώσετε τη διαδικασία ανάκτησης λογαριασμού.
 active_your_account=Ενεργοποιήστε Το Λογαριασμό Σας
 account_activated=Ο λογαριασμός έχει ενεργοποιηθεί
 prohibit_login=Απαγορεύεται η Σύνδεση
+prohibit_login_desc=Ο λογαριασμός σας δεν επιτρέπεται να συνδεθεί, παρακαλούμε επικοινωνήστε με το διαχειριστή σας.
 resent_limit_prompt=Έχετε ήδη ζητήσει ένα email ενεργοποίησης πρόσφατα. Παρακαλώ περιμένετε 3 λεπτά και προσπαθήστε ξανά.
 has_unconfirmed_mail=Γεια σας %s, έχετε μια ανεπιβεβαίωτη διεύθυνση ηλεκτρονικού ταχυδρομείου (<b>%s</b>). Εάν δεν έχετε λάβει email επιβεβαίωσης ή χρειάζεται να αποστείλετε εκ νέου ένα νέο, παρακαλώ κάντε κλικ στο παρακάτω κουμπί.
 resend_mail=Κάντε κλικ εδώ για να στείλετε ξανά το email ενεργοποίησης
@@ -365,8 +376,10 @@ email_not_associate=Η διεύθυνση ηλεκτρονικού ταχυδρ
 send_reset_mail=Αποστολή Email Ανάκτησης Λογαριασμού
 reset_password=Ανάκτηση Λογαριασμού
 invalid_code=Ο κωδικός επιβεβαίωσης δεν είναι έγκυρος ή έχει λήξει.
+invalid_code_forgot_password=Ο κωδικός επιβεβαίωσης δεν είναι έγκυρος ή έληξε. Πατήστε <a href="%s">εδώ</a> για να ξεκινήσετε νέα συνεδρία.
 invalid_password=Ο κωδικός πρόσβασης σας δεν ταιριάζει με τον κωδικό που χρησιμοποιήθηκε για τη δημιουργία του λογαριασμού.
 reset_password_helper=Ανάκτηση Λογαριασμού
+reset_password_wrong_user=Έχετε συνδεθεί ως %s, αλλά ο σύνδεσμος ανάκτησης λογαριασμού προορίζεται για το %s
 password_too_short=Το μήκος του κωδικού πρόσβασης δεν μπορεί να είναι μικρότερο από %d χαρακτήρες.
 non_local_account=Οι μη τοπικοί χρήστες δεν μπορούν να ενημερώσουν τον κωδικό πρόσβασής τους μέσω του διεπαφής web του Gitea.
 verify=Επαλήθευση
@@ -391,6 +404,7 @@ openid_connect_title=Σύνδεση σε υπάρχων λογαριασμό
 openid_connect_desc=Το επιλεγμένο OpenID URI είναι άγνωστο. Συνδέστε το με ένα νέο λογαριασμό εδώ.
 openid_register_title=Δημιουργία νέου λογαριασμού
 openid_register_desc=Το επιλεγμένο OpenID URI είναι άγνωστο. Συνδέστε το με ένα νέο λογαριασμό εδώ.
+openid_signin_desc=Εισάγετε το OpenID URI σας. Για παράδειγμα: alice.openid.example.org ή https://openid.example.org/alice.
 disable_forgot_password_mail=Η ανάκτηση λογαριασμού είναι απενεργοποιημένη επειδή δεν έχει οριστεί email. Παρακαλούμε επικοινωνήστε με το διαχειριστή.
 disable_forgot_password_mail_admin=Η ανάκτηση λογαριασμού είναι διαθέσιμη μόνο όταν έχει οριστεί το email. Παρακαλούμε ορίστει το email σας για να ενεργοποιήσετε την ανάκτηση λογαριασμού.
 email_domain_blacklisted=Δεν μπορείτε να εγγραφείτε με τη διεύθυνση email σας.
@@ -400,7 +414,9 @@ authorize_application_created_by=Αυτή η εφαρμογή δημιουργή
 authorize_application_description=Εάν παραχωρήσετε την πρόσβαση, θα μπορεί να έχει πρόσβαση και να γράφει σε όλες τις πληροφορίες του λογαριασμού σας, συμπεριλαμβανομένων των ιδιωτικών αποθετηρίων και οργανισμών.
 authorize_title=Εξουσιοδότηση του "%s" για έχει πρόσβαση στο λογαριασμό σας;
 authorization_failed=Αποτυχία εξουσιοδότησης
+authorization_failed_desc=Η εξουσιοδότηση απέτυχε επειδή εντοπίστηκε μια μη έγκυρη αίτηση. Παρακαλούμε επικοινωνήστε με το συντηρητή της εφαρμογής που προσπαθήσατε να εξουσιοδοτήσετε.
 sspi_auth_failed=Αποτυχία ταυτοποίησης SSPI
+password_pwned=Ο κωδικός πρόσβασης που επιλέξατε είναι σε μια λίστα <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">κλεμμένων κωδικών πρόσβασης</a> που προηγουμένως εκτέθηκαν σε παραβίαση δημόσιων δεδομένων. Παρακαλώ δοκιμάστε ξανά με διαφορετικό κωδικό πρόσβασης και σκεφτείτε να αλλάξετε αυτόν τον κωδικό πρόσβασης όπου αλλού χρησιμοποιείται.
 password_pwned_err=Δεν ήταν δυνατή η ολοκλήρωση του αιτήματος προς το HaveIBeenPwned
 
 [mail]
@@ -415,6 +431,7 @@ activate_account.text_1=Γεια σας <b>%[1]s</b>, ευχαριστούμε 
 activate_account.text_2=Παρακαλούμε κάντε κλικ στον παρακάτω σύνδεσμο για να ενεργοποιήσετε το λογαριασμό σας μέσα σε <b>%s</b>:
 
 activate_email=Επιβεβαιώστε τη διεύθυνση email σας
+activate_email.title=%s, παρακαλώ επαληθεύστε τη διεύθυνση email σας
 activate_email.text=Παρακαλώ κάντε κλικ στον παρακάτω σύνδεσμο για να επαληθεύσετε τη διεύθυνση email σας στο <b>%s</b>:
 
 register_notify=Καλώς ήλθατε στο Gitea
@@ -566,6 +583,7 @@ org_still_own_packages=Αυτός ο οργανισμός κατέχει ακό
 
 target_branch_not_exist=Ο κλάδος προορισμού δεν υπάρχει.
 
+
 [user]
 change_avatar=Αλλαγή του avatar σας…
 joined_on=Εγγράφηκε την %s
@@ -584,11 +602,14 @@ user_bio=Βιογραφικό
 disabled_public_activity=Αυτός ο χρήστης έχει απενεργοποιήσει τη δημόσια προβολή της δραστηριότητας.
 email_visibility.limited=Η διεύθυνση email σας είναι ορατή σε όλους τους ταυτοποιημένους χρήστες
 email_visibility.private=Η διεύθυνση email σας είναι ορατή μόνο σε εσάς και στους διαχειριστές
+show_on_map=Εμφάνιση της τοποθεσίας στο χάρτη
+settings=Ρυθμίσεις Χρήστη
 
 form.name_reserved=Το όνομα χρήστη "%s" είναι δεσμευμένο.
 form.name_pattern_not_allowed=Το μοτίβο "%s" δεν επιτρέπεται μέσα σε ένα όνομα χρήστη.
 form.name_chars_not_allowed=Το όνομα χρήστη "%s" περιέχει μη έγκυρους χαρακτήρες.
 
+
 [settings]
 profile=Προφίλ
 account=Λογαριασμός
@@ -605,9 +626,13 @@ delete=Διαγραφή Λογαριασμού
 twofa=Έλεγχος Ταυτότητας Δύο Παραγόντων
 account_link=Συνδεδεμένοι Λογαριασμοί
 organization=Οργανισμοί
+uid=UID
 webauthn=Κλειδιά Ασφαλείας
 
 public_profile=Δημόσιο Προφίλ
+biography_placeholder=Πείτε μας λίγο για τον εαυτό σας! (Μπορείτε να γράψετε με Markdown)
+location_placeholder=Μοιραστείτε την κατά προσέγγιση τοποθεσία σας με άλλους
+profile_desc=Ελέγξτε πώς εμφανίζεται το προφίλ σας σε άλλους χρήστες. Η κύρια διεύθυνση email σας θα χρησιμοποιηθεί για ειδοποιήσεις, ανάκτηση κωδικού πρόσβασης και λειτουργίες Git που βασίζονται στο web.
 password_username_disabled=Οι μη τοπικοί χρήστες δεν επιτρέπεται να αλλάξουν το όνομα χρήστη τους. Επικοινωνήστε με το διαχειριστή σας για περισσότερες λεπτομέρειες.
 full_name=Πλήρες Όνομα
 website=Ιστοσελίδα
@@ -619,6 +644,8 @@ update_language_not_found=Η γλώσσα "%s" δεν είναι διαθέσι
 update_language_success=Η γλώσσα ενημερώθηκε.
 update_profile_success=Το προφίλ σας έχει ενημερωθεί.
 change_username=Το όνομα χρήστη σας έχει αλλάξει.
+change_username_prompt=Σημείωση: Αλλάζοντας το όνομα χρήστη σας αλλάζει επίσης το URL του λογαριασμού σας.
+change_username_redirect_prompt=Το παλιό όνομα χρήστη θα ανακατευθύνει μέχρι να ζητηθεί ξανά.
 continue=Συνέχεια
 cancel=Ακύρωση
 language=Γλώσσα
@@ -643,6 +670,7 @@ comment_type_group_project=Έργο
 comment_type_group_issue_ref=Αναφορά ζητήματος
 saved_successfully=Οι ρυθμίσεις σας αποθηκεύτηκαν επιτυχώς.
 privacy=Απόρρητο
+keep_activity_private=Απόκρυψη Δραστηριότητας από τη σελίδα προφίλ
 keep_activity_private_popup=Με αυτή την επιλογή η δραστηριότητα σας είναι ορατή μόνο σε εσάς και τους διαχειριστές
 
 lookup_avatar_by_mail=Αναζήτηση ενός Avatar με διεύθυνση email
@@ -652,12 +680,14 @@ choose_new_avatar=Επιλέξτε νέα εικόνα
 update_avatar=Ενημέρωση Εικόνας
 delete_current_avatar=Διαγραφή Τρέχουσας Εικόνας
 uploaded_avatar_not_a_image=Το αρχείο που ανεβάσατε δεν είναι εικόνα.
+uploaded_avatar_is_too_big=Το μέγεθος αρχείου που ανέβηκε (%d KiB) υπερβαίνει το μέγιστο μέγεθος (%d KiB).
 update_avatar_success=Η εικόνα σας έχει ενημερωθεί.
 update_user_avatar_success=Το avatar του χρήστη ενημερώθηκε.
 
 change_password=Ενημέρωση Κωδικού Πρόσβασης
 old_password=Τρέχων Κωδικός Πρόσβασης
 new_password=Νέος Κωδικός Πρόσβασης
+retype_new_password=Επιβεβαίωση Νέου Κωδικού Πρόσβασης
 password_incorrect=Ο τρέχων κωδικός πρόσβασης είναι λάθος.
 change_password_success=Ο κωδικός πρόσβασής σας έχει ενημερωθεί. Από εδώ και τώρα συνδέεστε χρησιμοποιώντας τον νέο κωδικό πρόσβασής σας.
 password_change_disabled=Οι μη τοπικοί χρήστες δεν μπορούν να ενημερώσουν τον κωδικό πρόσβασής τους μέσω του διεπαφής web του Gitea.
@@ -666,6 +696,7 @@ emails=Διευθύνσεις Email
 manage_emails=Διαχείριση Διευθύνσεων Email
 manage_themes=Επιλέξτε προεπιλεγμένο θέμα διεπαφής
 manage_openid=Διαχείριση Διευθύνσεων OpenID
+email_desc=Η κύρια διεύθυνση ηλεκτρονικού ταχυδρομείου σας θα χρησιμοποιηθεί για ειδοποιήσεις, ανάκτηση του κωδικού πρόσβασης και, εφόσον δεν είναι κρυμμένη, λειτουργίες Git στον ιστότοπο.
 theme_desc=Αυτό θα είναι το προεπιλεγμένο θέμα διεπαφής σας σε όλη την ιστοσελίδα.
 primary=Κύριο
 activated=Ενεργό
@@ -673,6 +704,7 @@ requires_activation=Απαιτείται ενεργοποίηση
 primary_email=Αλλαγή κυριότητας
 activate_email=Αποστολή Ενεργοποίησης
 activations_pending=Εκκρεμούν Ενεργοποιήσεις
+can_not_add_email_activations_pending=Εκκρεμεί μια ενεργοποίηση, δοκιμάστε ξανά σε λίγα λεπτά αν θέλετε να προσθέσετε ένα νέο email.
 delete_email=Αφαίρεση
 email_deletion=Αφαίρεση Διεύθυνσης Email
 email_deletion_desc=Η διεύθυνση ηλεκτρονικού ταχυδρομείου και οι σχετικές πληροφορίες θα αφαιρεθούν από τον λογαριασμό σας. Οι υποβολές Git από αυτή τη διεύθυνση email θα παραμείνουν αμετάβλητες. Συνέχεια;
@@ -691,6 +723,7 @@ add_email_success=Η νέα διεύθυνση email έχει προστεθεί
 email_preference_set_success=Οι προτιμήσεις email έχουν οριστεί επιτυχώς.
 add_openid_success=Προστέθηκε η νέα διεύθυνση OpenID.
 keep_email_private=Απόκρυψη Διεύθυνσης Email
+keep_email_private_popup=Αυτό θα κρύψει τη διεύθυνση ηλεκτρονικού ταχυδρομείου σας από το προφίλ σας, καθώς και όταν κάνετε ένα pull request ή επεξεργαστείτε ένα αρχείο χρησιμοποιώντας τη διεπαφή ιστού. Οι ωθούμενες υποβολές δεν θα τροποποιηθούν. Χρησιμοποιήστε το %s στις υποβολές για να τις συσχετίσετε με το λογαριασμό σας.
 openid_desc=Το OpenID σας επιτρέπει να αναθέσετε τον έλεγχο ταυτότητας σε έναν εξωτερικό πάροχο.
 
 manage_ssh_keys=Διαχείριση SSH Κλειδιών
@@ -721,7 +754,6 @@ gpg_invalid_token_signature=Το κλειδί GPG, η υπογραφή και τ
 gpg_token_required=Πρέπει να δώσετε μια υπογραφή για το παρακάτω διακριτικό
 gpg_token=Διακριτικό
 gpg_token_help=Μπορείτε να δημιουργήσετε μια υπογραφή χρησιμοποιώντας:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Θωρακισμένη υπογραφή GPG
 key_signature_gpg_placeholder=Ξεκινά με '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=Το κλειδί GPG "%s" επαληθεύτηκε.
@@ -769,7 +801,9 @@ ssh_disabled=SSH Απενεργοποιημένο
 ssh_signonly=Το SSH είναι απενεργοποιημένο αυτή τη στιγμή, έτσι αυτά τα κλειδιά είναι μόνο για την επαλήθευση υπογραφής των υποβολών.
 ssh_externally_managed=Αυτό το κλειδί SSH διαχειρίζεται εξωτερικά για αυτόν το χρήστη
 manage_social=Διαχείριση Συσχετιζόμενων Λογαριασμών Κοινωνικών Δικτύων
+social_desc=Αυτοί οι κοινωνικοί λογαριασμοί μπορούν να χρησιμοποιηθούν για να συνδεθείτε στο λογαριασμό σας. Βεβαιωθείτε ότι τους αναγνωρίζετε όλους.
 unbind=Αποσύνδεση
+unbind_success=Ο κοινωνικός λογαριασμός έχει διαγραφεί επιτυχώς.
 
 manage_access_token=Διαχείριση Διακριτικών Πρόσβασης
 generate_new_token=Δημιουργία Νέου Διακριτικού
@@ -790,6 +824,8 @@ permissions_access_all=Όλα (δημόσια, ιδιωτικά, και περι
 select_permissions=Επιλέξτε δικαιώματα
 permission_no_access=Καμία Πρόσβαση
 permission_read=Αναγνωσμένες
+permission_write=Ανάγνωση και Εγγραφή
+access_token_desc=Τα επιλεγμένα δικαιώματα διακριτικών περιορίζουν την άδεια μόνο στις αντίστοιχες διαδρομές <a %s>API</a>. Διαβάστε την τεκμηρίωση <a %s></a> για περισσότερες πληροφορίες.
 at_least_one_permission=Πρέπει να επιλέξετε τουλάχιστον ένα δικαίωμα για να δημιουργήσετε ένα διακριτικό
 permissions_list=Δικαιώματα:
 
@@ -801,6 +837,8 @@ remove_oauth2_application_desc=Η αφαίρεση μιας εφαρμογής O
 remove_oauth2_application_success=Η εφαρμογή έχει διαγραφεί.
 create_oauth2_application=Δημιουργία νέας εφαρμογής OAuth2
 create_oauth2_application_button=Δημιουργία Εφαρμογής
+create_oauth2_application_success=Έχετε δημιουργήσει με επιτυχία μια νέα εφαρμογή OAuth2.
+update_oauth2_application_success=Έχετε ενημερώσει με επιτυχία την εφαρμογή OAuth2.
 oauth2_application_name=Όνομα Εφαρμογής
 oauth2_confidential_client=Εμπιστευτικός Πελάτης. Επιλέξτε το για εφαρμογές που διατηρούν το μυστικό κωδικό κρυφό, όπως πχ οι εφαρμογές ιστού. Μην επιλέγετε για εγγενείς εφαρμογές, συμπεριλαμβανομένων εφαρμογών επιφάνειας εργασίας και εφαρμογών για κινητά.
 oauth2_redirect_uris=URI Ανακατεύθυνσης. Χρησιμοποιήστε μια νέα γραμμή για κάθε URI.
@@ -809,19 +847,26 @@ oauth2_client_id=Ταυτότητα Πελάτη
 oauth2_client_secret=Μυστικό Πελάτη
 oauth2_regenerate_secret=Αναδημιουργία Μυστικού
 oauth2_regenerate_secret_hint=Χάσατε το μυστικό σας;
+oauth2_client_secret_hint=Το μυστικό δε θα εμφανιστεί ξανά αν κλείσετε ή ανανεώσετε αυτή τη σελίδα. Παρακαλώ βεβαιωθείτε ότι το έχετε αποθηκεύσει.
 oauth2_application_edit=Επεξεργασία
 oauth2_application_create_description=Οι εφαρμογές OAuth2 δίνει πρόσβαση στην εξωτερική εφαρμογή σας σε λογαριασμούς χρηστών σε αυτή την υπηρεσία.
+oauth2_application_remove_description=Αφαιρώντας μια εφαρμογή OAuth2 θα αποτραπεί η πρόσβαση αυτής, σε εξουσιοδοτημένους λογαριασμούς χρηστών σε αυτή την υπηρεσία. Συνέχεια;
+oauth2_application_locked=Το Gitea κάνει προεγγραφή σε μερικές εφαρμογές OAuth2 κατά την εκκίνηση αν είναι ενεργοποιημένες στις ρυθμίσεις. Για την αποφυγή απροσδόκητης συμπεριφοράς, αυτές δεν μπορούν ούτε να επεξεργαστούν ούτε να καταργηθούν. Παρακαλούμε ανατρέξτε στην τεκμηρίωση OAuth2 για περισσότερες πληροφορίες.
 
 authorized_oauth2_applications=Εξουσιοδοτημένες Εφαρμογές OAuth2
+authorized_oauth2_applications_description=Έχετε χορηγήσει πρόσβαση στον προσωπικό σας λογαριασμό σε αυτές τις εφαρμογές τρίτων. Ανακαλέστε την πρόσβαση για εφαρμογές που δεν χρειάζεστε πλέον.
 revoke_key=Ανάκληση
 revoke_oauth2_grant=Ανάκληση Πρόσβασης
 revoke_oauth2_grant_description=Η ανάκληση πρόσβασης για αυτή την εξωτερική εφαρμογή θα αποτρέψει αυτή την εφαρμογή από την πρόσβαση στα δεδομένα σας. Σίγουρα;
+revoke_oauth2_grant_success=Η πρόσβαση ανακλήθηκε επιτυχώς.
 
 twofa_desc=Ο έλεγχος ταυτότητας δύο παραγόντων ενισχύει την ασφάλεια του λογαριασμού σας.
+twofa_recovery_tip=Αν χάσετε τη συσκευή σας, θα είστε σε θέση να χρησιμοποιήσετε ένα κλειδί ανάκτησης μιας χρήσης για να ανακτήσετε την πρόσβαση στο λογαριασμό σας.
 twofa_is_enrolled=Ο λογαριασμός σας είναι <strong>εγγεγραμμένος</strong> σε έλεγχο ταυτότητας δύο παραγόντων.
 twofa_not_enrolled=Ο λογαριασμός σας δεν είναι εγγεγραμμένος σε έλεγχο ταυτότητας δύο παραγόντων.
 twofa_disable=Απενεργοποίηση Ταυτοποίησης Δύο Παραμέτρων
 twofa_scratch_token_regenerate=Αναδημιουργία Διακριτικού Μίας Χρήσης
+twofa_scratch_token_regenerated=Το κλειδί ανάκτησης μιας χρήσης είναι τώρα %s. Αποθηκεύστε το σε ασφαλές μέρος, καθώς δε θα εμφανιστεί ξανά.
 twofa_enroll=Εγγραφή στην ταυτοποίηση δύο παραγόντων
 twofa_disable_note=Μπορείτε να απενεργοποιήσετε την ταυτοποίηση δύο παραγόντων αν χρειαστεί.
 twofa_disable_desc=Η απενεργοποίηση της ταυτοποίησης δύο παραγόντων θα καταστήσει τον λογαριασμό σας λιγότερο ασφαλή. Συνέχεια;
@@ -839,6 +884,8 @@ webauthn_register_key=Προσθήκη Κλειδιού Ασφαλείας
 webauthn_nickname=Ψευδώνυμο
 webauthn_delete_key=Αφαίρεση Κλειδιού Ασφαλείας
 webauthn_delete_key_desc=Αν αφαιρέσετε ένα κλειδί ασφαλείας δεν μπορείτε πλέον να συνδεθείτε με αυτό. Συνέχεια;
+webauthn_key_loss_warning=Αν χάσετε τα κλειδιά ασφαλείας σας, θα χάσετε την πρόσβαση στο λογαριασμό σας.
+webauthn_alternative_tip=Μπορεί να θέλετε να ρυθμίσετε μια πρόσθετη μέθοδο ταυτοποίησης.
 
 manage_account_links=Διαχείριση Συνδεδεμένων Λογαριασμών
 manage_account_links_desc=Αυτοί οι εξωτερικοί λογαριασμοί είναι συνδεδεμένοι στον Gitea λογαριασμό σας.
@@ -848,8 +895,10 @@ remove_account_link=Αφαίρεση Συνδεδεμένου Λογαριασμ
 remove_account_link_desc=Η κατάργηση ενός συνδεδεμένου λογαριασμού θα ανακαλέσει την πρόσβασή του στο λογαριασμό σας στο Gitea. Συνέχεια;
 remove_account_link_success=Ο συνδεδεμένος λογαριασμός έχει αφαιρεθεί.
 
+hooks.desc=Προσθήκη webhooks που θα ενεργοποιούνται για <strong>όλα τα αποθετήρια</strong> που σας ανήκουν.
 
 orgs_none=Δεν είστε μέλος σε κάποιο οργανισμό.
+repos_none=Δεν κατέχετε κάποιο αποθετήριο.
 
 delete_account=Διαγραφή Του Λογαριασμού Σας
 delete_prompt=Αυτή η ενέργεια θα διαγράψει μόνιμα το λογαριασμό σας. <strong>ΔΕΝ ΘΑ ΜΠΟΡΕΙ</strong> να επανέλθει.
@@ -868,9 +917,12 @@ visibility=Ορατότητα χρήστη
 visibility.public=Δημόσια
 visibility.public_tooltip=Ορατό σε όλους
 visibility.limited=Περιορισμένη
+visibility.limited_tooltip=Ορατό μόνο στους ταυτοποιημένους χρήστες
 visibility.private=Ιδιωτική
+visibility.private_tooltip=Ορατό μόνο στα μέλη των οργανισμών που συμμετέχετε
 
 [repo]
+new_repo_helper=Ένα αποθετήριο περιέχει όλα τα αρχεία έργου, συμπεριλαμβανομένου του ιστορικού εκδόσεων. Ήδη φιλοξενείται αλλού; <a href="%s">Μετεγκατάσταση αποθετηρίου.</a>
 owner=Ιδιοκτήτης
 owner_helper=Ορισμένοι οργανισμοί ενδέχεται να μην εμφανίζονται στο αναπτυσσόμενο μενού λόγω του μέγιστου αριθμού αποθετηρίων.
 repo_name=Όνομα αποθετηρίου
@@ -882,6 +934,7 @@ template_helper=Μετατροπή σε πρότυπο αποθετήριο
 template_description=Τα πρότυπα αποθετήρια επιτρέπουν στους χρήστες να δημιουργήσουν νέα αποθετήρια με την ίδια δομή, αρχεία και προαιρετικές ρυθμίσεις.
 visibility=Ορατότητα
 visibility_description=Μόνο ο ιδιοκτήτης ή τα μέλη του οργανισμού εάν έχουν δικαιώματα, θα είναι σε θέση να το δουν.
+visibility_helper=Αλλάξτε το αποθετήριο σε ιδιωτικό
 visibility_helper_forced=Ο διαχειριστής σας αναγκάζει τα νέα αποθετήρια να είναι ιδιωτικά.
 visibility_fork_helper=(Αλλάζοντας αυτό θα επηρεάσει όλα τα forks.)
 clone_helper=Χρειάζεστε βοήθεια για τη κλωνοποίηση; Επισκεφθείτε τη <a target="_blank" rel="noopener noreferrer" href="%s">Βοήθεια</a>.
@@ -890,8 +943,10 @@ fork_from=Fork Από Το
 already_forked=Έχετε ήδη κάνει fork το %s
 fork_to_different_account=Fork σε διαφορετικό λογαριασμό
 fork_visibility_helper=Η ορατότητα ενός fork αποθετηρίου δεν μπορεί να αλλάξει.
+fork_branch=Κλάδος που θα κλωνοποιηθεί στο fork
+all_branches=Όλοι οι κλάδοι
+fork_no_valid_owners=Αυτό το αποθετήριο δεν μπορεί να γίνει fork επειδή δεν υπάρχουν έγκυροι ιδιοκτήτες.
 use_template=Χρήση αυτού του πρότυπου
-clone_in_vsc=Κλωνοποίηση στο VS Code
 download_zip=Λήψη ZIP
 download_tar=Λήψη TAR.GZ
 download_bundle=Κατεβάστε Το ΔΕΜΑ
@@ -918,6 +973,7 @@ trust_model_helper_collaborator_committer=Συνεργάτης+Υποβολέα
 trust_model_helper_default=Προεπιλογή: Χρησιμοποιήστε το προεπιλεγμένο μοντέλο εμπιστοσύνης για αυτήν την εγκατάσταση
 create_repo=Δημιουργία Αποθετηρίου
 default_branch=Προεπιλεγμένος Κλάδος
+default_branch_label=προεπιλογή
 default_branch_helper=Ο προεπιλεγμένος κλάδος είναι ο βασικός κλάδος για pull requests και υποβολές κώδικα.
 mirror_prune=Καθαρισμός
 mirror_prune_desc=Αφαίρεση παρωχημένων αναφορών απομακρυσμένης-παρακολούθησης
@@ -926,6 +982,8 @@ mirror_interval_invalid=Το χρονικό διάστημα του ειδώλο
 mirror_sync_on_commit=Συγχρονισμός κατά την ώθηση
 mirror_address=Κλωνοποίηση Από Το URL
 mirror_address_desc=Τοποθετήστε όλα τα απαιτούμενα διαπιστευτήρια στην ενότητα Εξουσιοδότηση.
+mirror_address_url_invalid=Η διεύθυνση URL που δόθηκε δεν είναι έγκυρη. Πρέπει να κάνετε escape όλα τα στοιχεία του url σωστά.
+mirror_address_protocol_invalid=Η παρεχόμενη διεύθυνση URL δεν είναι έγκυρη. Μόνο οι τοποθεσίες http(s):// ή git:// μπορούν να χρησιμοποιηθούν για τη δημιουργία ειδώλου.
 mirror_lfs=Large File Storage (LFS)
 mirror_lfs_desc=Ενεργοποίηση αντικατοπτρισμού δεδομένων LFS.
 mirror_lfs_endpoint=Άκρο LFS
@@ -951,13 +1009,20 @@ delete_preexisting=Διαγραφή αρχείων που προϋπήρχαν
 delete_preexisting_content=Διαγραφή αρχείων στο %s
 delete_preexisting_success=Διαγράφηκαν τα μη υιοθετημένα αρχεία στο %s
 blame_prior=Προβολή ευθύνης πριν από αυτή την αλλαγή
+blame.ignore_revs=Αγνόηση των αναθεωρήσεων στο <a href="%s">.git-blame-ignore-revs</a>. Πατήστε <a href="%s">εδώ</a> για να το παρακάμψετε και να δείτε την κανονική προβολή ευθυνών.
+blame.ignore_revs.failed=Αποτυχία αγνόησης των αναθεωρήσεων στο <a href="%s">.git-blame-ignore-revs</a>.
 author_search_tooltip=Εμφάνιση το πολύ 30 χρηστών
 
+tree_path_not_found_commit=Η διαδρομή %[1]s δεν υπάρχει στην υποβολή %[2]s
+tree_path_not_found_branch=Η διαδρομή %[1]s δεν υπάρχει στον κλάδο %[2]s
+tree_path_not_found_tag=Η διαδρομή %[1]s δεν υπάρχει στην ετικέτα %[2]s
 
 transfer.accept=Αποδοχή Μεταφοράς
 transfer.accept_desc=`Μεταφορά στο "%s"`
 transfer.reject=Απόρριψη Μεταφοράς
 transfer.reject_desc=`Ακύρωση μεταφοράς σε "%s"`
+transfer.no_permission_to_accept=Δεν έχετε άδεια να αποδεχτείτε αυτή τη μεταφορά.
+transfer.no_permission_to_reject=Δεν έχετε άδεια να απορρίψετε αυτή τη μεταφορά.
 
 desc.private=Ιδιωτικό
 desc.public=Δημόσιο
@@ -976,6 +1041,8 @@ template.issue_labels=Σήματα Ζητήματος
 template.one_item=Πρέπει να επιλέξετε τουλάχιστον ένα αντικείμενο στο πρότυπο
 template.invalid=Πρέπει να επιλέξετε ένα πρότυπο αποθετήριο
 
+archive.title=Αυτό το αποθετήρειο αρχειοθετήθηκε. Μπορείτε να προβάλετε αρχεία και να τα κλωνοποιήσετε, αλλά δεν μπορείτε να ωθήσετε ή να ανοίξετε ζητήματα ή pull requests.
+archive.title_date=Αυτό το αποθετήριο έχει αρχειοθετηθεί στο %s. Μπορείτε να προβάλετε αρχεία και να κλωνοποιήσετε, αλλά δεν μπορείτε να ωθήσετε ή να ανοίξετε ζητήματα ή pull requests.
 archive.issue.nocomment=Αυτό το αποθετήριο αρχειοθετήθηκε. Δεν μπορείτε να σχολιάσετε σε ζητήματα.
 archive.pull.nocomment=Αυτό το repo αρχειοθετήθηκε. Δεν μπορείτε να σχολιάσετε στα pull requests.
 
@@ -992,6 +1059,7 @@ migrate_options_lfs=Μεταφορά αρχείων LFS
 migrate_options_lfs_endpoint.label=Άκρο LFS
 migrate_options_lfs_endpoint.description=Η μεταφορά θα προσπαθήσει να χρησιμοποιήσει το Git remote για να <a target="_blank" rel="noopener noreferrer" href="%s">καθορίσει τον διακομιστή LFS</a>. Μπορείτε επίσης να καθορίσετε ένα δικό σας endpoint αν τα δεδομένα LFS του αποθετηρίου αποθηκεύονται κάπου αλλού.
 migrate_options_lfs_endpoint.description.local=Μια διαδρομή στο τοπικό διακομιστή επίσης υποστηρίζεται.
+migrate_options_lfs_endpoint.placeholder=Αν αφεθεί κενό, το άκρο θα προκύψει από το URL του κλώνου
 migrate_items=Στοιχεία Μεταφοράς
 migrate_items_wiki=Wiki
 migrate_items_milestones=Ορόσημα
@@ -1094,6 +1162,10 @@ file_view_rendered=Προβολή Απόδοσης
 file_view_raw=Προβολή Ακατέργαστου
 file_permalink=Permalink
 file_too_large=Το αρχείο είναι πολύ μεγάλο για να εμφανιστεί.
+invisible_runes_header=`Αυτό το αρχείο περιέχει αόρατους χαρακτήρες Unicode `
+invisible_runes_description=`Αυτό το αρχείο περιέχει αόρατους χαρακτήρες Unicode που δεν διακρίνονται από ανθρώπους, αλλά μπορεί να επεξεργάζονται διαφορετικά από έναν υπολογιστή. Αν νομίζετε ότι αυτό είναι σκόπιμο, μπορείτε να αγνοήσετε με ασφάλεια αυτή την προειδοποίηση. Χρησιμοποιήστε το κουμπί Escape για να τους αποκαλύψετε.`
+ambiguous_runes_header=`Αυτό το αρχείο περιέχει ασαφείς χαρακτήρες Unicode `
+ambiguous_runes_description=`Αυτό το αρχείο περιέχει χαρακτήρες Unicode που μπορεί να συγχέονται με άλλους χαρακτήρες. Αν νομίζετε ότι αυτό είναι σκόπιμο, μπορείτε να αγνοήσετε με ασφάλεια αυτή την προειδοποίηση. Χρησιμοποιήστε το κουμπί Escape για να τους αποκαλύψετε.`
 invisible_runes_line=`Αυτή η γραμμή έχει αόρατους χαρακτήρες unicode `
 ambiguous_runes_line=`Αυτή η γραμμή έχει ασαφείς χαρακτήρες unicode `
 ambiguous_character=`ο %[1]c [U+%04[1]X] μπορεί να μπερδευτεί με τον %[2]c [U+%04[2]X]`
@@ -1106,11 +1178,15 @@ video_not_supported_in_browser=Το πρόγραμμα περιήγησής σα
 audio_not_supported_in_browser=Το πρόγραμμα περιήγησής σας δεν υποστηρίζει την ετικέτα HTML5 'audio'.
 stored_lfs=Αποθηκεύτηκε με το Git LFS
 symbolic_link=Symbolic link
+executable_file=Εκτελέσιμο Αρχείο
 commit_graph=Γράφημα Υποβολών
 commit_graph.select=Επιλογή κλάδων
 commit_graph.hide_pr_refs=Απόκρυψη Pull Requests
 commit_graph.monochrome=Μονόχρωμο
 commit_graph.color=Έγχρωμο
+commit.contained_in=Αυτή η υποβολή περιλαμβάνεται σε:
+commit.contained_in_default_branch=Αυτή η υποβολή είναι μέρος του προεπιλεγμένου κλάδου
+commit.load_referencing_branches_and_tags=Φόρτωση κλάδων και ετικετών που παραπέμπουν σε αυτήν την υποβολή
 blame=Ευθύνη
 download_file=Λήψη αρχείου
 normal_view=Κανονική Προβολή
@@ -1189,9 +1265,7 @@ commits.desc=Δείτε το ιστορικό αλλαγών του πηγαίο
 commits.commits=Υποβολές
 commits.no_commits=Δεν υπάρχουν κοινές υποβολές. Τα "%s" και "%s" έχουν εντελώς διαφορετικές ιστορίες.
 commits.nothing_to_compare=Αυτοί οι κλάδοι είναι όμοιοι.
-commits.search=Αναζήτηση υποβολών…
 commits.search.tooltip=Μπορείτε να προθέτετε τις λέξεις-κλειδιά με "author:", "committer:", "after:", ή "before:", π.χ. "επαναφορά author:Alice before:2019-01-13".
-commits.find=Αναζήτηση
 commits.search_all=Όλοι Οι Κλάδοι
 commits.author=Συγγραφέας
 commits.message=Μήνυμα
@@ -1203,6 +1277,7 @@ commits.signed_by_untrusted_user=Υπογράφηκε από μη έμπιστο
 commits.signed_by_untrusted_user_unmatched=Υπογράφηκε από ένα μη έμπιστο χρήστη ο οποίος δεν ταιριάζει με τον υποβολέα
 commits.gpg_key_id=ID Κλειδιού GPG
 commits.ssh_key_fingerprint=Αποτύπωμα Κλειδιού SSH
+commits.view_path=Προβολή σε αυτή τη στιγμή στο ιστορικό
 
 commit.operations=Λειτουργίες
 commit.revert=Απόσυρση
@@ -1241,7 +1316,6 @@ projects.type.basic_kanban=Βασικό Kanban
 projects.type.bug_triage=Διαλογή Σφαλμάτων
 projects.template.desc=Πρότυπο έργου
 projects.template.desc_helper=Επιλέξτε ένα πρότυπο έργου για να ξεκινήσετε
-projects.type.uncategorized=Χωρίς Κατηγορία
 projects.column.edit=Επεξεργασία Στήλης
 projects.column.edit_title=Όνομα
 projects.column.new_title=Όνομα
@@ -1249,10 +1323,7 @@ projects.column.new_submit=Δημιουργία Στήλης
 projects.column.new=Νέα Στήλη
 projects.column.set_default=Ορισμός Προεπιλογής
 projects.column.set_default_desc=Ορίστε αυτή τη στήλη ως προεπιλογή για ζητήματα και pull requests χωρίς κατηγορία
-projects.column.unset_default=Αφαίρεση Προεπιλογής
-projects.column.unset_default_desc=Αφαίρεση της προεπιλογής αυτής της στήλης
 projects.column.delete=Διαγραφή Στήλης
-projects.column.deletion_desc=Η διαγραφή μιας στήλης έργου μετακινεί όλα τα συναφή ζητήματα σε 'Χωρίς Κατηγορία'. Συνέχεια;
 projects.column.color=Έγχρωμο
 projects.open=Άνοιγμα
 projects.close=Κλείσιμο
@@ -1330,6 +1401,7 @@ issues.delete_branch_at=`διέγραψε το κλάδο <b>%s</b> %s`
 issues.filter_label=Σήμα
 issues.filter_label_exclude=`Χρησιμοποιήστε <code>alt</code> + <code>κάντε κλικ/Enter</code> για να εξαιρέσετε τις σημάνσεις`
 issues.filter_label_no_select=Όλα τα σήματα
+issues.filter_label_select_no_label=Χωρίς ετικέτα
 issues.filter_milestone=Ορόσημο
 issues.filter_milestone_all=Όλα τα ορόσημα
 issues.filter_milestone_none=Χωρίς ορόσημα
@@ -1380,9 +1452,10 @@ issues.opened_by_fake=άνοιξε το %[1]s από %[2]s
 issues.closed_by_fake=από %[2]s έκλεισαν %[1]s
 issues.previous=Προηγούμενο
 issues.next=Επόμενο
-issues.open_title=Ανοιχτά
+issues.open_title=Ανοικτό
 issues.closed_title=Κλειστά
 issues.draft_title=Προσχέδιο
+issues.num_comments_1=%d σχόλιο
 issues.num_comments=%d σχόλια
 issues.commented_at=`σχολίασε <a href="#%s">%s</a>`
 issues.delete_comment_confirm=Θέλετε σίγουρα να διαγράψετε αυτό το σχόλιο;
@@ -1391,6 +1464,7 @@ issues.context.quote_reply=Παράθεση Απάντησης
 issues.context.reference_issue=Αναφορά σε νέο ζήτημα
 issues.context.edit=Επεξεργασία
 issues.context.delete=Διαγραφή
+issues.no_content=Δεν υπάρχει περιγραφή.
 issues.close=Κλείσιμο Ζητήματος
 issues.comment_pull_merged_at=συγχώνευσε την υποβολή %[1]s στο %[2]s %[3]s
 issues.comment_manually_pull_merged_at=συγχώνευσε χειροκίνητα την υποβολή %[1]s στο %[2]s %[3]s
@@ -1409,8 +1483,17 @@ issues.ref_closed_from=`<a href="%[3]s">έκλεισε αυτό το ζήτημ
 issues.ref_reopened_from=`<a href="%[3]s">άνοιξε ξανά αυτό το ζήτημα %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_from=`από %[1]s`
 issues.author=Συγγραφέας
+issues.author_helper=Αυτός ο χρήστης είναι ο συγγραφέας.
 issues.role.owner=Ιδιοκτήτης
+issues.role.owner_helper=Αυτός ο χρήστης είναι ο ιδιοκτήτης αυτού του αποθετηρίου.
 issues.role.member=Μέλος
+issues.role.member_helper=Αυτός ο χρήστης είναι μέλος του οργανισμού που κατέχει αυτό το αποθετήριο.
+issues.role.collaborator=Συνεργάτης
+issues.role.collaborator_helper=Αυτός ο χρήστης έχει προσκληθεί να συνεργαστεί στο αποθετήριο.
+issues.role.first_time_contributor=Συντελεστής για πρώτη φορά
+issues.role.first_time_contributor_helper=Αυτή είναι η πρώτη συνεισφορά αυτού του χρήστη στο αποθετήριο.
+issues.role.contributor=Συντελεστής
+issues.role.contributor_helper=Αυτός ο χρήστης έχει προηγούμενές υποβολές στο αποθετήριο.
 issues.re_request_review=Επαναίτηση ανασκόπησης
 issues.is_stale=Έχουν υπάρξει αλλαγές σε αυτό το PR από αυτή την αναθεώρηση
 issues.remove_request_review=Αφαίρεση αιτήματος αναθεώρησης
@@ -1425,6 +1508,9 @@ issues.label_title=Όνομα σήματος
 issues.label_description=Περιγραφή σήματος
 issues.label_color=Χρώμα σήματος
 issues.label_exclusive=Αποκλειστικό
+issues.label_archive=Αρχειοθέτηση Σήματος
+issues.label_archived_filter=Εμφάνιση αρχειοθετημένων σημάτων
+issues.label_archive_tooltip=Τα αρχειοθετημένα σήματα εξαιρούνται από τις προτάσεις στην αναζήτηση με σήματα.
 issues.label_exclusive_desc=Ονομάστε το σήμα <code>πεδίο/στοιχείο</code> για να το κάνετε αμοιβαία αποκλειστικό με άλλα σήματα <code>πεδίου/</code>.
 issues.label_exclusive_warning=Τυχόν συγκρουόμενα σήματα θα αφαιρεθούν κατά την επεξεργασία των σημάτων ενός ζητήματος ή pull request.
 issues.label_count=%d σήματα
@@ -1479,6 +1565,7 @@ issues.tracking_already_started=`Έχετε ήδη ξεκινήσει την κ
 issues.stop_tracking=Διακοπή Χρονομέτρου
 issues.stop_tracking_history=`σταμάτησε να εργάζεται %s`
 issues.cancel_tracking=Απόρριψη
+issues.cancel_tracking_history=`ακύρωσε τη παρακολούθηση χρόνου %s`
 issues.add_time=Χειροκίνητη Προσθήκη Ώρας
 issues.del_time=Διαγραφή αυτού του αρχείου χρόνου
 issues.add_time_short=Προσθήκη Χρόνου
@@ -1502,6 +1589,7 @@ issues.due_date_form=εεεε-μμ-ηη
 issues.due_date_form_add=Προσθήκη ημερομηνίας παράδοσης
 issues.due_date_form_edit=Επεξεργασία
 issues.due_date_form_remove=Διαγραφή
+issues.due_date_not_writer=Χρειάζεστε πρόσβαση εγγραφής στο αποθετήριο για να ενημερώσετε την ημερομηνία λήξης ενός προβλήματος.
 issues.due_date_not_set=Δεν ορίστηκε ημερομηνία παράδοσης.
 issues.due_date_added=πρόσθεσε την ημερομηνία παράδοσης %s %s
 issues.due_date_modified=τροποποίησε την ημερομηνία παράδοσης από %[2]s σε %[1]s %[3]s
@@ -1557,6 +1645,9 @@ issues.review.pending.tooltip=Αυτό το σχόλιο προς το παρό
 issues.review.review=Αξιολόγηση
 issues.review.reviewers=Εξεταστές
 issues.review.outdated=Παρωχημένο
+issues.review.outdated_description=Το περιεχόμενο άλλαξε αφού έγινε αυτό το σχόλιο
+issues.review.option.show_outdated_comments=Εμφάνιση παρωχημένων σχολίων
+issues.review.option.hide_outdated_comments=Απόκρυψη παρωχημένων σχολίων
 issues.review.show_outdated=Εμφάνιση παροχημένων
 issues.review.hide_outdated=Απόκρυψη παροχημένων
 issues.review.show_resolved=Εμφάνιση επιλυμένων
@@ -1595,7 +1686,13 @@ pulls.compare_compare=τράβηγμα από
 pulls.switch_comparison_type=Αλλαγή τύπου σύγκρισης
 pulls.switch_head_and_base=Αλλαγή κεφαλής και βάσης
 pulls.filter_branch=Φιλτράρισμα κλάδου
-pulls.no_results=Δεν βρέθηκαν αποτελέσματα.
+pulls.show_all_commits=Εμφάνιση όλων των υποβολών
+pulls.show_changes_since_your_last_review=Εμφάνιση αλλαγών από την τελευταία αξιολόγηση
+pulls.showing_only_single_commit=Εμφάνιση μόνο αλλαγών της υποβολής %[1]s
+pulls.showing_specified_commit_range=Εμφάνιση μόνο των αλλαγών μεταξύ %[1]s..%[2]s
+pulls.select_commit_hold_shift_for_range=Επιλέξτε υποβολή. Κρατήστε πατημένο το shift + κάντε κλικ για να επιλέξετε ένα εύρος
+pulls.review_only_possible_for_full_diff=Η αξιολόγηση είναι δυνατή μόνο κατά την προβολή της πλήρης διαφοράς
+pulls.filter_changes_by_commit=Φιλτράρισμα κατά υποβολή
 pulls.nothing_to_compare=Αυτοί οι κλάδοι είναι όμοιοι. Δεν υπάρχει ανάγκη να δημιουργήσετε ένα pull request.
 pulls.nothing_to_compare_and_allow_empty_pr=Αυτοί οι κλάδοι είναι ίσοι. Αυτό το PR θα είναι κενό.
 pulls.has_pull_request=`Υπάρχει ήδη pull request μεταξύ αυτών των κλάδων: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1627,6 +1724,12 @@ pulls.is_empty=Οι αλλαγές σε αυτόν τον κλάδο είναι
 pulls.required_status_check_failed=Ορισμένοι απαιτούμενοι έλεγχοι δεν ήταν επιτυχείς.
 pulls.required_status_check_missing=Λείπουν ορισμένοι απαιτούμενοι έλεγχοι.
 pulls.required_status_check_administrator=Ως διαχειριστής, μπορείτε ακόμα να συγχωνεύσετε αυτό το pull request.
+pulls.blocked_by_approvals=Το pull request δεν έχει ακόμα αρκετές εγκρίσεις. Δόθηκαν %d από %d εγκρίσεις.
+pulls.blocked_by_rejection=Αυτό το Pull Request έχει αλλαγές που ζητούνται από έναν επίσημο εξεταστή.
+pulls.blocked_by_official_review_requests=Αυτό το Pull Request έχει επίσημες αιτήσεις αξιολόγησης.
+pulls.blocked_by_outdated_branch=Αυτό το pull request έχει αποκλειστεί επειδή είναι παρωχημένο.
+pulls.blocked_by_changed_protected_files_1=Αυτό το pull request έχει αποκλειστεί επειδή αλλάζει ένα προστατευμένο αρχείο:
+pulls.blocked_by_changed_protected_files_n=Αυτό το pull request έχει αποκλειστεί επειδή αλλάζει προστατευμένα αρχεία:
 pulls.can_auto_merge_desc=Αυτό το Pull Request μπορεί να συγχωνευθεί αυτόματα.
 pulls.cannot_auto_merge_desc=Αυτό το pull request δεν μπορεί να συγχωνευθεί αυτόματα λόγω συγκρούσεων.
 pulls.cannot_auto_merge_helper=Χειροκίνητη Συγχώνευση για την επίλυση των συγκρούσεων.
@@ -1661,6 +1764,7 @@ pulls.rebase_conflict_summary=Μήνυμα Σφάλματος
 pulls.unrelated_histories=H Συγχώνευση Απέτυχε: Η κεφαλή και η βάση της συγχώνευσης δεν μοιράζονται μια κοινή ιστορία. Συμβουλή: Δοκιμάστε μια διαφορετική στρατηγική
 pulls.merge_out_of_date=Η συγχώνευση απέτυχε: Κατά τη δημιουργία της συγχώνευσης, η βάση ενημερώθηκε. Συμβουλή: Δοκιμάστε ξανά.
 pulls.head_out_of_date=Η συγχώνευση απέτυχε: Κατά τη δημιουργία της συγχώνευσης, το HEAD ενημερώθηκε. Συμβουλή: Δοκιμάστε ξανά.
+pulls.has_merged=Αποτυχία: Το pull request έχει συγχωνευθεί, δεν είναι δυνατή η συγχώνευση ξανά ή να αλλάξει ο κλάδος προορισμού.
 pulls.push_rejected=Η συγχώνευση απέτυχε: Η ώθηση απορρίφθηκε. Ελέγξτε τα Άγκιστρα Git για αυτό το αποθετήριο.
 pulls.push_rejected_summary=Μήνυμα Πλήρους Απόρριψης
 pulls.push_rejected_no_message=H Συγχώνευση Aπέτυχε: Η ώθηση απορρίφθηκε, αλλά δεν υπήρχε απομακρυσμένο μήνυμα.<br>Ελέγξτε τα Άγκιστρα Git για αυτό το αποθετήριο
@@ -1672,6 +1776,8 @@ pulls.status_checks_failure=Κάποιοι έλεγχοι απέτυχαν
 pulls.status_checks_error=Ορισμένοι έλεγχοι ανέφεραν σφάλματα
 pulls.status_checks_requested=Απαιτείται
 pulls.status_checks_details=Λεπτομέρειες
+pulls.status_checks_hide_all=Απόκρυψη όλων των ελέγχων
+pulls.status_checks_show_all=Εμφάνιση όλων των ελέγχων
 pulls.update_branch=Ενημέρωση κλάδου με συγχώνευση
 pulls.update_branch_rebase=Ενημέρωση κλάδου με rebase
 pulls.update_branch_success=Η ενημέρωση του κλάδου ήταν επιτυχής
@@ -1680,6 +1786,11 @@ pulls.outdated_with_base_branch=Αυτός ο κλάδος δεν είναι ε
 pulls.close=Κλείσιμο Pull Request
 pulls.closed_at=`έκλεισε αυτό το pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 pulls.reopened_at=`άνοιξε ξανά αυτό το pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.cmd_instruction_hint=`Δείτε τις <a class="show-instruction">οδηγίες γραμμής εντολών</a>.`
+pulls.cmd_instruction_checkout_title=Έλεγχος
+pulls.cmd_instruction_checkout_desc=Από το αποθετήριο του έργου σας, ελέγξτε έναν νέο κλάδο και δοκιμάστε τις αλλαγές.
+pulls.cmd_instruction_merge_title=Συγχώνευση
+pulls.cmd_instruction_merge_desc=Συγχώνευση των αλλαγών και ενημέρωση στο Gitea.
 pulls.clear_merge_message=Εκκαθάριση μηνύματος συγχώνευσης
 pulls.clear_merge_message_hint=Η εκκαθάριση του μηνύματος συγχώνευσης θα αφαιρέσει μόνο το περιεχόμενο του μηνύματος υποβολής και θα διατηρήσει τα παραγόμενα git trailers όπως "Co-Authored-By …".
 
@@ -1698,7 +1809,9 @@ pulls.auto_merge_canceled_schedule_comment=`ακύρωσε την αυτόματ
 pulls.delete.title=Διαγραφή αυτού του pull request;
 pulls.delete.text=Θέλετε πραγματικά να διαγράψετε αυτό το pull request; (Αυτό θα σβήσει οριστικά όλο το περιεχόμενο του. Εξετάστε αν θέλετε να το κλείσετε, αν σκοπεύεται να το αρχειοθετήσετε)
 
+pulls.recently_pushed_new_branches=Ωθήσατε στο κλάδο <strong>%[1]s</strong> %[2]s
 
+pull.deleted_branch=(διαγράφηκε):%s
 
 milestones.new=Νέο Ορόσημο
 milestones.closed=Έκλεισε %s
@@ -1706,6 +1819,7 @@ milestones.update_ago=Ενημερώθηκε %s
 milestones.no_due_date=Δεν υπάρχει ημερομηνία παράδοσης
 milestones.open=Άνοιγμα
 milestones.close=Κλείσιμο
+milestones.new_subheader=Τα ορόσημα μπορούν να σας βοηθήσουν να οργανώσετε τα ζητήματα και να παρακολουθείτε την πρόοδό τους.
 milestones.completeness=%d%% Ολοκληρώθηκε
 milestones.create=Δημιουργία Ορόσημου
 milestones.title=Τίτλος
@@ -1722,11 +1836,26 @@ milestones.edit_success=Το ορόσημο "%s" ενημερώθηκε.
 milestones.deletion=Διαγραφή Ορόσημου
 milestones.deletion_desc=Η διαγραφή ενός ορόσημου το αφαιρεί από όλα τα συναφή ζητήματα. Συνέχεια;
 milestones.deletion_success=Το ορόσημο έχει διαγραφεί.
+milestones.filter_sort.earliest_due_data=Πλησιέστερη παράδοση
+milestones.filter_sort.latest_due_date=Απώτερη παράδοση
 milestones.filter_sort.least_complete=Λιγότερο πλήρη
 milestones.filter_sort.most_complete=Περισσότερο πλήρη
 milestones.filter_sort.most_issues=Περισσότερα ζητήματα
 milestones.filter_sort.least_issues=Λιγότερα ζητήματα
 
+signing.will_sign=Αυτή η υποβολή θα υπογραφεί με το κλειδί "%s".
+signing.wont_sign.error=Παρουσιάστηκε σφάλμα κατά τον έλεγχο για το αν η υποβολή μπορεί να υπογραφεί.
+signing.wont_sign.nokey=Δεν υπάρχει διαθέσιμο κλειδί για να υπογραφεί αυτή η υποβολή.
+signing.wont_sign.never=Οι υποβολές δεν υπογράφονται ποτέ.
+signing.wont_sign.always=Οι υποβολές υπογράφονται πάντα.
+signing.wont_sign.pubkey=Η υποβολή δε θα υπογραφεί επειδή δεν υπάρχει δημόσιο κλειδί που να συνδέεται με το λογαριασμό σας.
+signing.wont_sign.twofa=Πρέπει να έχετε ενεργοποιημένη την ταυτοποίηση δύο παραγόντων για να υπογράφεται υποβολές.
+signing.wont_sign.parentsigned=Η υποβολή δε θα υπογραφεί καθώς η γονική υποβολή δεν έχει υπογραφεί.
+signing.wont_sign.basesigned=Η συγχώνευση δε θα υπογραφεί καθώς η βασική υποβολή δεν έχει υπογραφή της βάσης.
+signing.wont_sign.headsigned=Η συγχώνευση δε θα υπογραφεί καθώς δεν έχει υπογραφή η υποβολή της κεφαλής.
+signing.wont_sign.commitssigned=Η συγχώνευση δε θα υπογραφεί καθώς όλες οι σχετικές υποβολές δεν έχουν υπογραφεί.
+signing.wont_sign.approved=Η συγχώνευση δε θα υπογραφεί καθώς το PR δεν έχει εγκριθεί.
+signing.wont_sign.not_signed_in=Δεν είστε συνδεδεμένοι.
 
 ext_wiki=Πρόσβαση στο Εξωτερικό Wiki
 ext_wiki.desc=Σύνδεση σε ένα εξωτερικό wiki.
@@ -1824,16 +1953,7 @@ activity.git_stats_and_deletions=και
 activity.git_stats_deletion_1=%d διαγραφή
 activity.git_stats_deletion_n=%d διαγραφές
 
-search=Αναζήτηση
-search.search_repo=Αναζήτηση αποθετηρίου
-search.type.tooltip=Τύπος αναζήτησης
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Συμπερίληψη και των αποτελεσμάτων που είναι πλησιέστερα με τον όρο αναζήτησης
-search.match=Ταίριασμα
-search.match.tooltip=Συμπερίληψη μόνο των αποτελεσμάτων που ταιριάζουν ακριβώς με τον όρο αναζήτησης
-search.results=Αποτελέσματα αναζήτησης για "%s" σε <a href="%s">%s</a>
-search.code_no_results=Δεν βρέθηκε πηγαίος κώδικας που να ταιριάζει με τον όρο αναζήτησης.
-search.code_search_unavailable=Η αναζήτηση κώδικα δεν είναι διαθέσιμη αυτή τη στιγμή. Παρακαλώ επικοινωνήστε με το διαχειριστή.
+contributors.contribution_type.commits=Υποβολές
 
 settings=Ρυθμίσεις
 settings.desc=Στις Ρυθμίσεις μπορείτε να διαχειριστείτε τις ρυθμίσεις για το αποθετήριο
@@ -1856,7 +1976,9 @@ settings.mirror_settings.docs.disabled_push_mirror.info=Τα είδωλα ώθη
 settings.mirror_settings.docs.no_new_mirrors=Το αποθετήριο σας αντιγράφει τις αλλαγές προς ή από ένα άλλο αποθετήριο. Λάβετε υπόψη ότι δεν μπορείτε να δημιουργήσετε νέα είδωλα αυτή τη στιγμή.
 settings.mirror_settings.docs.can_still_use=Αν και δεν μπορείτε να τροποποιήσετε τα υπάρχοντα είδωλα ή να δημιουργήσετε νέα, μπορείτε να χρησιμοποιείται ακόμα το υπάρχων είδωλο.
 settings.mirror_settings.docs.pull_mirror_instructions=Για να ορίσετε έναν είδωλο έλξης, παρακαλούμε συμβουλευθείτε:
+settings.mirror_settings.docs.more_information_if_disabled=Μπορείτε να μάθετε περισσότερα για τα είδωλα ώθησης και έλξης εδώ:
 settings.mirror_settings.docs.doc_link_title=Πώς μπορώ να αντιγράψω αποθετήρια;
+settings.mirror_settings.docs.doc_link_pull_section=το κεφάλαιο "Pulling from a remote repository" της τεκμηρίωσης.
 settings.mirror_settings.docs.pulling_remote_title=Έλξη από ένα απομακρυσμένο αποθετήριο
 settings.mirror_settings.mirrored_repository=Είδωλο αποθετηρίου
 settings.mirror_settings.direction=Κατεύθυνση
@@ -1866,8 +1988,11 @@ settings.mirror_settings.last_update=Τελευταία ενημέρωση
 settings.mirror_settings.push_mirror.none=Δεν έχουν ρυθμιστεί είδωλα ώθησης
 settings.mirror_settings.push_mirror.remote_url=URL Απομακρυσμένου Αποθετηρίου Git
 settings.mirror_settings.push_mirror.add=Προσθήκη Είδωλου Push
+settings.mirror_settings.push_mirror.edit_sync_time=Επεξεργασία διαστήματος συγχρονισμού ειδώλου
 
 settings.sync_mirror=Συγχρονισμός Τώρα
+settings.pull_mirror_sync_in_progress=Έλκονται αλλαγές από το απομακρυσμένο %s αυτή τη στιγμή.
+settings.push_mirror_sync_in_progress=Ώθηση αλλαγών στο απομακρυσμένο %s αυτή τη στιγμή.
 settings.site=Ιστοσελίδα
 settings.update_settings=Ενημέρωση Ρυθμίσεων
 settings.update_mirror_settings=Ενημέρωση Ρυθμίσεων Ειδώλου
@@ -1907,6 +2032,7 @@ settings.pulls.default_allow_edits_from_maintainers=Να επιτρέποντα
 settings.releases_desc=Ενεργοποίηση Κυκλοφοριών Αποθετηρίου
 settings.packages_desc=Ενεργοποίηση Μητρώου Πακέτων Αποθετηρίου
 settings.projects_desc=Ενεργοποίηση Έργων Αποθετηρίου
+settings.projects_mode_all=Όλα τα έργα
 settings.actions_desc=Ενεργοποίηση Δράσεων Αποθετηρίου
 settings.admin_settings=Ρυθμίσεις Διαχειριστή
 settings.admin_enable_health_check=Ενεργοποίηση Ελέγχων Υγείας του Αποθετηρίου (git fsck)
@@ -1934,6 +2060,7 @@ settings.transfer.rejected=Η μεταβίβαση του αποθετηρίου
 settings.transfer.success=Η μεταβίβαση του αποθετηρίου ήταν επιτυχής.
 settings.transfer_abort=Ακύρωση μεταβίβασης
 settings.transfer_abort_invalid=Δεν μπορείτε να ακυρώσετε μια ανύπαρκτη μεταβίβαση αποθετηρίου.
+settings.transfer_abort_success=Η μεταφορά αποθετηρίου στο %s ακυρώθηκε με επιτυχία.
 settings.transfer_desc=Μεταβιβάστε αυτό το αποθετήριο σε έναν χρήστη ή σε έναν οργανισμό για τον οποίο έχετε δικαιώματα διαχειριστή.
 settings.transfer_form_title=Εισάγετε το όνομα του αποθετηρίου ως επιβεβαίωση:
 settings.transfer_in_progress=Αυτή τη στιγμή υπάρχει μια εν εξελίξει μεταβίβαση. Παρακαλούμε ακυρώστε την αν θέλετε να μεταβιβάσετε αυτό το αποθετήριο σε άλλο χρήστη.
@@ -1980,7 +2107,6 @@ settings.delete_collaborator=Αφαίρεση
 settings.collaborator_deletion=Αφαίρεση Συνεργάτη
 settings.collaborator_deletion_desc=Η κατάργηση ενός συνεργάτη θα ανακαλέσει την πρόσβασή τους σε αυτό το αποθετήριο. Συνέχεια;
 settings.remove_collaborator_success=Ο συνεργάτης έχει αφαιρεθεί.
-settings.search_user_placeholder=Αναζήτηση χρήστη…
 settings.org_not_allowed_to_be_collaborator=Οι οργανισμοί δεν μπορούν να προστεθούν ως συνεργάτης.
 settings.change_team_access_not_allowed=Η αλλαγή της πρόσβασης ομάδας για το αποθετήριο έχει περιοριστεί στον ιδιοκτήτη του οργανισμού
 settings.team_not_in_organization=Η ομάδα δεν είναι στον ίδιο οργανισμό με το αποθετήριο
@@ -1988,7 +2114,6 @@ settings.teams=Ομάδες
 settings.add_team=Προσθήκη Ομάδας
 settings.add_team_duplicate=Η ομάδα έχει ήδη το αποθετήριο
 settings.add_team_success=Η ομάδα έχει πλέον πρόσβαση στο αποθετήριο.
-settings.search_team=Αναζήτηση Ομάδας…
 settings.change_team_permission_tip=Τα δικαιώματα της ομάδας έχουν οριστεί στη σελίδα ρυθμίσεων της ομάδας και δεν μπορούν να αλλάξουν ανά αποθετήριο
 settings.delete_team_tip=Αυτή η ομάδα έχει πρόσβαση σε όλα τα αποθετήρια και δεν μπορεί να αφαιρεθεί
 settings.remove_team_success=Έχει αφαιρεθεί η πρόσβαση της ομάδας στο αποθετήριο.
@@ -2000,12 +2125,14 @@ settings.webhook_deletion_desc=Η αφαίρεση ενός webhook διαγρά
 settings.webhook_deletion_success=Το webhook έχει αφαιρεθεί.
 settings.webhook.test_delivery=Δοκιμή Παράδοσης
 settings.webhook.test_delivery_desc=Δοκιμάστε αυτό το webhook με ένα ψεύτικο συμβάν.
+settings.webhook.test_delivery_desc_disabled=Για να δοκιμάσετε αυτό το webhook με μια ψεύτικη κλήση, ενεργοποιήστε το.
 settings.webhook.request=Αίτημα
 settings.webhook.response=Απάντηση
 settings.webhook.headers=Κεφαλίδες
 settings.webhook.payload=Περιεχόμενο
 settings.webhook.body=Σώμα
 settings.webhook.replay.description=Επανάληψη αυτού του webhook.
+settings.webhook.replay.description_disabled=Για να επαναλάβετε αυτό το webhook, ενεργοποιήστε το.
 settings.webhook.delivery.success=Ένα γεγονός έχει προστεθεί στην ουρά παράδοσης. Μπορεί να χρειαστούν λίγα δευτερόλεπτα μέχρι να εμφανιστεί στο ιστορικό.
 settings.githooks_desc=Τα Άγκιστρα Git παρέχονται από το ίδιο το Git. Μπορείτε να επεξεργαστείτε τα αρχεία αγκίστρων παρακάτω για να ρυθμίσετε προσαρμοσμένες λειτουργίες.
 settings.githook_edit_desc=Αν το hook είναι ανενεργό, θα παρουσιαστεί ένα παράδειγμα. Αφήνοντας το περιεχόμενο του hook κενό θα το απενεργοποιήσετε.
@@ -2139,9 +2266,7 @@ settings.protect_whitelist_committers=Περιορισμός του Push στη
 settings.protect_whitelist_committers_desc=Μόνο χρήστες ή ομάδες στη λίστα θα επιτρέπεται να κάνουν push σε αυτόν τον κλάδο (αλλά όχι να κάνουν force push).
 settings.protect_whitelist_deploy_keys=Έγκριση κλειδιών διάθεσης με πρόσβαση εγγραφής για ώθηση.
 settings.protect_whitelist_users=Λίστα χρηστών που επιτρέπεται να κάνουν push:
-settings.protect_whitelist_search_users=Αναζήτηση χρηστών…
 settings.protect_whitelist_teams=Λίστα ομάδων που επιτρέπεται να κάνουν push:
-settings.protect_whitelist_search_teams=Αναζήτηση ομάδων…
 settings.protect_merge_whitelist_committers=Ενεργοποίηση Λίστας Συγχώνευσης
 settings.protect_merge_whitelist_committers_desc=Επιτρέψτε μόνο σε χρήστες ή ομάδες στη λίστα να συγχωνεύσουν pull requests σε αυτό το κλάδο.
 settings.protect_merge_whitelist_users=Λίστα επιτρεπόμενων χρηστών για συγχώνευση:
@@ -2165,6 +2290,7 @@ settings.dismiss_stale_approvals_desc=Όταν οι νέες υποβολές π
 settings.require_signed_commits=Απαιτούνται Υπογεγραμμένες Υποβολές
 settings.require_signed_commits_desc=Απόρριψη νέων υποβολών σε αυτόν τον κλάδο εάν είναι μη υπογεγραμμένες ή μη επαληθεύσιμες.
 settings.protect_branch_name_pattern=Μοτίβο Προστατευμένου Ονόματος Κλάδου
+settings.protect_branch_name_pattern_desc=Μοτίβα ονόματος προστατευμένων κλάδων. Δείτε <a href="https://github.com/gobwas/glob">την τεκμηρίωση</a> για σύνταξη μοτίβου. Παραδείγματα: main, release/**
 settings.protect_patterns=Μοτίβα
 settings.protect_protected_file_patterns=Μοτίβα προστατευμένων αρχείων (διαχωρισμένα με ερωτηματικό ';'):
 settings.protect_protected_file_patterns_desc=Τα προστατευόμενα αρχεία δεν επιτρέπεται να αλλάξουν άμεσα, ακόμη και αν ο χρήστης έχει δικαιώματα να προσθέσει, να επεξεργαστεί ή να διαγράψει αρχεία σε αυτόν τον κλάδο. Επιπλέων μοτίβα μπορούν να διαχωριστούν με ερωτηματικό (';'). Δείτε την τεκμηρίωση <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> για τη σύνταξη του μοτίβου. Πχ: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
@@ -2201,18 +2327,26 @@ settings.tags.protection.allowed.teams=Επιτρεπόμενες ομάδες
 settings.tags.protection.allowed.noone=Καμία
 settings.tags.protection.create=Προστασία Ετικέτας
 settings.tags.protection.none=Δεν υπάρχουν προστατευμένες ετικέτες.
+settings.tags.protection.pattern.description=Μπορείτε να χρησιμοποιήσετε ένα μόνο όνομα ή ένα μοτίβο τύπου glob ή κανονική έκφραση για να ταιριάξετε πολλαπλές ετικέτες. Διαβάστε περισσότερα στον <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">οδηγό προστατευμένων ετικετών</a>.
 settings.bot_token=Διακριτικό Bot
 settings.chat_id=ID Συνομιλίας
+settings.thread_id=ID Νήματος
 settings.matrix.homeserver_url=Homeserver URL
 settings.matrix.room_id=ID Δωματίου
 settings.matrix.message_type=Τύπος Μηνύματος
 settings.archive.button=Αρχειοθέτηση Αποθετηρίου
 settings.archive.header=Αρχειοθέτηση Αυτού του Αποθετηρίου
+settings.archive.text=Η αρχειοθέτηση του αποθετηρίου θα το αλλάξει σε μόνο για ανάγνωση. Δε θα φαίνεται στον αρχικό πίνακα. Κανείς (ακόμα και εσείς!) δε θα μπορεί να κάνει νέες υποβολές, ή να ανοίξει ζητήματα ή pull request.
 settings.archive.success=Το αποθετήριο αρχειοθετήθηκε με επιτυχία.
 settings.archive.error=Παρουσιάστηκε σφάλμα κατά την προσπάθεια αρχειοθέτησης του αποθετηρίου. Δείτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες.
 settings.archive.error_ismirror=Δε μπορείτε να αρχειοθετήσετε ένα είδωλο αποθετηρίου.
 settings.archive.branchsettings_unavailable=Οι ρυθμίσεις του κλάδου δεν είναι διαθέσιμες αν το αποθετήριο είναι αρχειοθετημένο.
 settings.archive.tagsettings_unavailable=Οι ρυθμίσεις της ετικέτας δεν είναι διαθέσιμες αν το αποθετήριο είναι αρχειοθετημένο.
+settings.unarchive.button=Απο-Αρχειοθέτηση αποθετηρίου
+settings.unarchive.header=Απο-Αρχειοθέτηση του αποθετηρίου
+settings.unarchive.text=Η απο-αρχειοθέτηση του αποθετηρίου θα αποκαταστήσει την ικανότητά του να λαμβάνει υποβολές και ωθήσεις, καθώς και νέα ζητήματα και pull-requests.
+settings.unarchive.success=Το αποθετήριο απο-αρχειοθετήθηκε με επιτυχία.
+settings.unarchive.error=Παρουσιάστηκε σφάλμα κατά την προσπάθεια απο-αρχειοθέτησης του αποθετηρίου. Δείτε τις καταγραφές για περισσότερες λεπτομέρειες.
 settings.update_avatar_success=Η εικόνα του αποθετηρίου έχει ενημερωθεί.
 settings.lfs=LFS
 settings.lfs_filelist=Αρχεία LFS σε αυτό το αποθετήριο
@@ -2279,6 +2413,7 @@ diff.show_more=Εμφάνιση Περισσότερων
 diff.load=Φόρτωση Διαφορών
 diff.generated=δημιουργημένο
 diff.vendored=εξωτερικό
+diff.comment.add_line_comment=Προσθήκη σχολίου στη γραμμή
 diff.comment.placeholder=Αφήστε ένα σχόλιο
 diff.comment.markdown_info=Υποστηρίζεται στυλ με markdown.
 diff.comment.add_single_comment=Προσθέστε ένα σχόλιο
@@ -2335,6 +2470,7 @@ release.edit_release=Ενημέρωση Κυκλοφορίας
 release.delete_release=Διαγραφή Κυκλοφορίας
 release.delete_tag=Διαγραφή Ετικέτας
 release.deletion=Διαγραφή Κυκλοφορίας
+release.deletion_desc=Διαγράφοντας μια κυκλοφορία, αυτή αφαιρείται μόνο από το Gitea. Δε θα επηρεάσει την ετικέτα Git, τα περιεχόμενα του αποθετηρίου σας ή το ιστορικό της. Συνέχεια;
 release.deletion_success=Η κυκλοφορία έχει διαγραφεί.
 release.deletion_tag_desc=Θα διαγράψει αυτή την ετικέτα από το αποθετήριο. Τα περιεχόμενα του αποθετηρίου και το ιστορικό παραμένουν αμετάβλητα. Συνέχεια;
 release.deletion_tag_success=Η ετικέτα έχει διαγραφεί.
@@ -2354,6 +2490,7 @@ branch.already_exists=Ήδη υπάρχει ένας κλάδος με το όν
 branch.delete_head=Διαγραφή
 branch.delete=`Διαγραφή του Κλάδου "%s"`
 branch.delete_html=Διαγραφή Κλάδου
+branch.delete_desc=Η διαγραφή ενός κλάδου είναι μόνιμη. Αν και ο διαγραμμένος κλάδος μπορεί να συνεχίσει να υπάρχει για σύντομο χρονικό διάστημα πριν να αφαιρεθεί, ΔΕΝ ΜΠΟΡΕΙ να αναιρεθεί στις περισσότερες περιπτώσεις. Συνέχεια;
 branch.deletion_success=Ο κλάδος "%s" διαγράφηκε.
 branch.deletion_failed=Αποτυχία διαγραφής του κλάδου "%s".
 branch.delete_branch_has_new_commits=Ο κλάδος "%s" δεν μπορεί να διαγραφεί επειδή προστέθηκαν νέες υποβολές μετά τη συγχώνευση.
@@ -2393,6 +2530,7 @@ tag.create_success=Η ετικέτα "%s" δημιουργήθηκε.
 topic.manage_topics=Διαχείριση Θεμάτων
 topic.done=Ολοκληρώθηκε
 topic.count_prompt=Δεν μπορείτε να επιλέξετε περισσότερα από 25 θέματα
+topic.format_prompt=Τα θέματα πρέπει να ξεκινούν με γράμμα ή αριθμό, μπορούν να περιλαμβάνουν παύλες ('-') και τελείες ('.'), μπορεί να είναι μέχρι 35 χαρακτήρες. Τα γράμματα πρέπει να είναι πεζά.
 
 find_file.go_to_file=Αναζήτηση αρχείου
 find_file.no_matching=Δεν ταιριάζει κανένα αρχείο
@@ -2401,6 +2539,8 @@ error.csv.too_large=Δεν είναι δυνατή η απόδοση αυτού
 error.csv.unexpected=Δεν είναι δυνατή η απόδοση αυτού του αρχείου, επειδή περιέχει έναν μη αναμενόμενο χαρακτήρα στη γραμμή %d και στη στήλη %d.
 error.csv.invalid_field_count=Δεν είναι δυνατή η απόδοση αυτού του αρχείου, επειδή έχει λάθος αριθμό πεδίων στη γραμμή %d.
 
+[graphs]
+
 [org]
 org_name_holder=Όνομα Οργανισμού
 org_full_name_holder=Πλήρες Όνομα Οργανισμού
@@ -2431,6 +2571,7 @@ form.create_org_not_allowed=Δεν επιτρέπεται να δημιουργ
 settings=Ρυθμίσεις
 settings.options=Οργανισμός
 settings.full_name=Πλήρες Όνομα
+settings.email=Email Επικοινωνίας
 settings.website=Ιστοσελίδα
 settings.location=Τοποθεσία
 settings.permission=Δικαιώματα
@@ -2444,6 +2585,7 @@ settings.visibility.private_shortname=Ιδιωτικός
 
 settings.update_settings=Ενημέρωση Ρυθμίσεων
 settings.update_setting_success=Οι ρυθμίσεις του οργανισμού έχουν ενημερωθεί.
+settings.change_orgname_prompt=Σημείωση: Η αλλαγή του ονόματος του οργανισμού θα αλλάξει επίσης τη διεύθυνση URL του οργανισμού σας και θα απελευθερώσει το παλιό όνομα.
 settings.change_orgname_redirect_prompt=Το παλιό όνομα θα ανακατευθύνει μέχρι να διεκδικηθεί.
 settings.update_avatar_success=Η εικόνα του οργανισμού έχει ενημερωθεί.
 settings.delete=Διαγραφή Οργανισμού
@@ -2503,7 +2645,6 @@ teams.write_permission_desc=Αυτή η ομάδα χορηγεί πρόσβασ
 teams.admin_permission_desc=Αυτή η ομάδα παρέχει πρόσβαση <strong>Διαχειριστή</strong>: τα μέλη μπορούν να διαβάσουν, να κάνουν push και να προσθέσουν συνεργάτες στα αποθετήρια της ομάδας.
 teams.create_repo_permission_desc=Επιπλέον, αυτή η ομάδα χορηγεί άδεια <strong>Δημιουργία αποθετηρίου</strong>: τα μέλη μπορούν να δημιουργήσουν νέα αποθετήρια στον οργανισμό.
 teams.repositories=Αποθετήρια Ομάδας
-teams.search_repo_placeholder=Αναζήτηση αποθετηρίου…
 teams.remove_all_repos_title=Αφαίρεση όλων των αποθετηρίων της ομάδας
 teams.remove_all_repos_desc=Αυτό θα αφαιρέσει όλα τα αποθετήρια από την ομάδα.
 teams.add_all_repos_title=Προσθήκη όλων των αποθετηρίων
@@ -2519,27 +2660,33 @@ teams.all_repositories_helper=Η ομάδα έχει πρόσβαση σε όλ
 teams.all_repositories_read_permission_desc=Αυτή η ομάδα χορηγεί πρόσβαση <strong>Ανάγνωσης</strong> σε <strong>όλα τα αποθετήρια</strong>: τα μέλη μπορούν να δουν και να κλωνοποιήσουν αποθετήρια.
 teams.all_repositories_write_permission_desc=Αυτή η ομάδα χορηγεί πρόσβαση <strong>Εγγραφής</strong> σε <strong>όλα τα αποθετήρια</strong>: τα μέλη μπορούν να διαβάσουν και να κάνουν push σε αποθετήρια.
 teams.all_repositories_admin_permission_desc=Αυτή η ομάδα παρέχει πρόσβαση <strong>Διαχείρισης</strong> σε <strong>όλα τα αποθετήρια</strong>: τα μέλη μπορούν να διαβάσουν, να κάνουν push και να προσθέσουν συνεργάτες στα αποθετήρια.
+teams.invite.title=Έχετε προσκληθεί να συμμετάσχετε στην ομάδα <strong>%s</strong> του οργανισμού <strong>%s</strong>.
 teams.invite.by=Προσκλήθηκε από %s
 teams.invite.description=Παρακαλώ κάντε κλικ στον παρακάτω σύνδεσμο για συμμετοχή στην ομάδα.
 
 [admin]
 dashboard=Πίνακας Ελέγχου
+identity_access=Ταυτότητα & Πρόσβαση
 users=Λογαριασμοί Χρήστη
 organizations=Οργανισμοί
+assets=Στοιχεία Κώδικα
 repositories=Αποθετήρια
 hooks=Webhooks
+integrations=Ενσωματώσεις
 authentication=Πηγές Ταυτοποίησης
 emails=Email Χρήστη
 config=Διαμόρφωση
+config_summary=Περίληψη
+config_settings=Ρυθμίσεις
 notices=Ειδοποιήσεις Συστήματος
 monitor=Παρακολούθηση
 first_page=Πρώτο
 last_page=Τελευταίο
 total=Σύνολο: %d
+settings=Ρυθμίσεις Διαχειριστή
 
 dashboard.new_version_hint=Το Gitea %s είναι διαθέσιμο, τώρα εκτελείτε το %s. Ανατρέξτε <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">στο blog</a> για περισσότερες λεπτομέρειες.
 dashboard.statistic=Περίληψη
-dashboard.operations=Λειτουργίες Συντήρησης
 dashboard.system_status=Κατάσταση Συστήματος
 dashboard.operation_name=Όνομα Λειτουργίας
 dashboard.operation_switch=Αλλαγή
@@ -2548,11 +2695,13 @@ dashboard.clean_unbind_oauth=Εκκαθάριση μη δεσμευμένων σ
 dashboard.clean_unbind_oauth_success=Όλες οι μη δεσμευμένες συνδέσεις OAuth διαγράφηκαν.
 dashboard.task.started=Εκκίνηση Εργασίας: %[1]s
 dashboard.task.process=Εργασία: %[1]s
+dashboard.task.cancelled=Εργασία: %[1]ακυρώθηκε: %[3]s
 dashboard.task.error=Σφάλμα στην Εργασία: %[1]s: %[3]s
 dashboard.task.finished=Εργασία: %[1]s που εκκινήθηκε από %[2]s τελείωσε
 dashboard.task.unknown=Άγνωστη εργασία: %[1]s
 dashboard.cron.started=Εκκίνηση Προγραμματισμένης Εργασίας: %[1]s
 dashboard.cron.process=Προγραμματισμένη Εργασία: %[1]s
+dashboard.cron.cancelled=Προγραμματισμένη εργασία: %[1]s ακυρώθηκε: %[3]s
 dashboard.cron.error=Σφάλμα στη Προγραμματισμένη Εργασία: %s: %[3]s
 dashboard.cron.finished=Προγραμματισμένη Εργασία: %[1]s τελείωσε
 dashboard.delete_inactive_accounts=Διαγραφή όλων των μη ενεργοποιημένων λογαριασμών
@@ -2562,6 +2711,7 @@ dashboard.delete_repo_archives.started=Η διαγραφή όλων των αρ
 dashboard.delete_missing_repos=Διαγραφή όλων των αποθετηρίων που δεν έχουν τα αρχεία Git τους
 dashboard.delete_missing_repos.started=Η διαγραφή όλων των αποθετηρίων που δεν έχουν αρχεία Git τους, ξεκίνησε.
 dashboard.delete_generated_repository_avatars=Διαγραφή δημιουργημένων εικόνων αποθετηρίων
+dashboard.sync_repo_branches=Συγχρονισμός κλάδων που λείπουν, από τα δεδομένα git στις βάσεις δεδομένων
 dashboard.update_mirrors=Ενημέρωση Ειδώλων
 dashboard.repo_health_check=Έλεγχος υγείας σε όλα τα αποθετήρια
 dashboard.check_repo_stats=Έλεγχος όλων των στατιστικών αποθετηρίων
@@ -2576,6 +2726,7 @@ dashboard.reinit_missing_repos=Επανεκκινήστε όλα τα αποθε
 dashboard.sync_external_users=Συγχρονισμός δεδομένων εξωτερικών χρηστών
 dashboard.cleanup_hook_task_table=Εκκαθάριση πίνακα hook_task
 dashboard.cleanup_packages=Εκκαθάριση ληγμένων πακέτων
+dashboard.cleanup_actions=Οι ενέργειες καθαρισμού καταγραφές και αντικείμενα
 dashboard.server_uptime=Διάρκεια Διακομιστή
 dashboard.current_goroutine=Τρέχουσες Goroutines
 dashboard.current_memory_usage=Τρέχουσα Χρήση Μνήμης
@@ -2613,6 +2764,9 @@ dashboard.gc_lfs=Συλλογή απορριμάτων στα μετα-αντι
 dashboard.stop_zombie_tasks=Διακοπή εργασιών ζόμπι
 dashboard.stop_endless_tasks=Διακοπή ατελείωτων εργασιών
 dashboard.cancel_abandoned_jobs=Ακύρωση εγκαταλελειμμένων εργασιών
+dashboard.start_schedule_tasks=Έναρξη προγραμματισμένων εργασιών
+dashboard.sync_branch.started=Ο Συγχρονισμός των Κλάδων ξεκίνησε
+dashboard.rebuild_issue_indexer=Αναδόμηση ευρετηρίου ζητημάτων
 
 users.user_manage_panel=Διαχείριση Λογαριασμών Χρηστών
 users.new_account=Δημιουργία Λογαριασμού Χρήστη
@@ -2621,6 +2775,9 @@ users.full_name=Πλήρες Όνομα
 users.activated=Ενεργοποιήθηκε
 users.admin=Διαχειριστής
 users.restricted=Περιορισμένος
+users.reserved=Δεσμευμένο
+users.bot=Bot
+users.remote=Απομακρυσμένο
 users.2fa=2FA
 users.repos=Αποθετήρια
 users.created=Δημιουργήθηκε
@@ -2667,6 +2824,7 @@ users.list_status_filter.is_prohibit_login=Απαγόρευση Σύνδεσης
 users.list_status_filter.not_prohibit_login=Επιτρέπεται η Σύνδεση
 users.list_status_filter.is_2fa_enabled=2FA Ενεργοποιημένο
 users.list_status_filter.not_2fa_enabled=2FA Απενεργοποιημένο
+users.details=Λεπτομέρειες Χρήστη
 
 emails.email_manage_panel=Διαχείριση Email Χρήστη
 emails.primary=Κύριο
@@ -2679,6 +2837,7 @@ emails.updated=Το email ενημερώθηκε
 emails.not_updated=Αποτυχία ενημέρωσης της ζητούμενης διεύθυνσης email: %v
 emails.duplicate_active=Αυτή η διεύθυνση email είναι ήδη ενεργή σε διαφορετικό χρήστη.
 emails.change_email_header=Ενημέρωση Ιδιοτήτων Email
+emails.change_email_text=Είστε βέβαιοι ότι θέλετε να ενημερώσετε αυτή τη διεύθυνση email;
 
 orgs.org_manage_panel=Διαχείριση Οργανισμού
 orgs.name=Όνομα
@@ -2692,15 +2851,15 @@ repos.unadopted.no_more=Δεν βρέθηκαν μη υιοθετημένα απ
 repos.owner=Ιδιοκτήτης
 repos.name=Όνομα
 repos.private=Ιδιωτικό
-repos.watches=Παρακολουθήσεις
-repos.stars=Αστέρια
-repos.forks=Forks
 repos.issues=Ζητήματα
 repos.size=Μέγεθος
+repos.lfs_size=Μέγεθος LFS
 
 packages.package_manage_panel=Διαχείριση Πακέτων
 packages.total_size=Συνολικό Μέγεθος: %s
 packages.unreferenced_size=Μέγεθος Χωρίς Αναφορά: %s
+packages.cleanup=Εκκαθάριση ληγμένων δεδομένων
+packages.cleanup.success=Επιτυχής εκκαθάριση δεδομένων που έχουν λήξει
 packages.owner=Ιδιοκτήτης
 packages.creator=Δημιουργός
 packages.name=Όνομα
@@ -2711,10 +2870,12 @@ packages.size=Μέγεθος
 packages.published=Δημοσιευμένα
 
 defaulthooks=Προεπιλεγμένα Webhooks
+defaulthooks.desc=Τα Webhooks κάνουν αυτόματα αιτήσεις HTTP POST σε ένα διακομιστή όταν ενεργοποιούν ορισμένα γεγονότα στο Gitea. Τα Webhooks που ορίζονται εδώ είναι προκαθορισμένα και θα αντιγραφούν σε όλα τα νέα αποθετήρια. Διαβάστε περισσότερα στον οδηγό <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">webhooks</a>.
 defaulthooks.add_webhook=Προσθήκη Προεπιλεγμένου Webhook
 defaulthooks.update_webhook=Ενημέρωση Προεπιλεγμένου Webhook
 
 systemhooks=Webhooks Συστήματος
+systemhooks.desc=Τα Webhooks κάνουν αυτόματα αιτήσεις HTTP POST σε ένα διακομιστή όταν ενεργοποιούνται ορισμένα γεγονότα στο Gitea. Τα Webhooks που ορίζονται εδώ θα ενεργούν σε όλα τα αποθετήρια του συστήματος, γι 'αυτό παρακαλώ εξετάστε τυχόν επιπτώσεις απόδοσης που μπορεί να έχει. Διαβάστε περισσότερα στον οδηγό <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">webhooks</a>.
 systemhooks.add_webhook=Προσθήκη Webhook Συστήματος
 systemhooks.update_webhook=Ενημέρωση Webhook Συστήματος
 
@@ -2807,17 +2968,18 @@ auths.sspi_default_language=Προεπιλεγμένη γλώσσα χρήστη
 auths.sspi_default_language_helper=Προεπιλεγμένη γλώσσα για τους χρήστες που δημιουργούνται αυτόματα με τη μέθοδο ταυτοποίησης SSPI. Αφήστε κενό αν προτιμάτε η γλώσσα να εντοπιστεί αυτόματα.
 auths.tips=Συμβουλές
 auths.tips.oauth2.general=Ταυτοποίηση OAuth2
+auths.tips.oauth2.general.tip=Κατά την εγγραφή μιας νέας ταυτοποίησης OAuth2, το URL κλήσης/ανακατεύθυνσης πρέπει να είναι:
 auths.tip.oauth2_provider=Πάροχος OAuth2
 auths.tip.bitbucket=Καταχωρήστε ένα νέο καταναλωτή OAuth στο https://bitbucket.org/account/user/<your username>/oauth-consumers/new και προσθέστε το δικαίωμα 'Account' - 'Read'
 auths.tip.nextcloud=`Καταχωρήστε ένα νέο καταναλωτή OAuth στην υπηρεσία σας χρησιμοποιώντας το παρακάτω μενού "Settings -> Security -> OAuth 2.0 client"`
 auths.tip.dropbox=Δημιουργήστε μια νέα εφαρμογή στο https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Καταχωρήστε μια νέα εφαρμογή στο https://developers.facebook.com/apps και προσθέστε το προϊόν "Facebook Login"`
 auths.tip.github=Καταχωρήστε μια νέα εφαρμογή OAuth στο https://github.com/settings/applications/new
-auths.tip.gitlab=Καταχωρήστε μια νέα εφαρμογή στο https://gitlab.com/profile/applications
 auths.tip.google_plus=Αποκτήστε τα διαπιστευτήρια πελάτη OAuth2 από την κονσόλα API της Google στο https://console.developers.google.com/
 auths.tip.openid_connect=Χρησιμοποιήστε το OpenID Connect Discovery URL (<server>/.well known/openid-configuration) για να καθορίσετε τα τελικά σημεία
 auths.tip.twitter=Πηγαίνετε στο https://dev.twitter.com/apps, δημιουργήστε μια εφαρμογή και βεβαιωθείτε ότι η επιλογή “Allow this application to be used to Sign in with Twitter” είναι ενεργοποιημένη
 auths.tip.discord=Καταχωρήστε μια νέα εφαρμογή στο https://discordapp.com/developers/applications/me
+auths.tip.gitea=Καταχωρήστε μια νέα εφαρμογή OAuth2. Μπορείτε να βρείτε τον οδηγό στο https://docs.gitea.com/development/oauth2-provider
 auths.tip.yandex=`Δημιουργήστε μια νέα εφαρμογή στο https://oauth.yandex.com/client/new. Επιλέξτε τα ακόλουθα δικαιώματα από την ενότητα "Yandex.Passport API": "Access to email address", "Access to user avatar" και "Access to username, first name and surname, gender"`
 auths.tip.mastodon=Εισαγάγετε ένα προσαρμομένο URL για την υπηρεσία mastodon με την οποία θέλετε να πιστοποιήσετε (ή να χρησιμοποιήσετε την προεπιλεγμένη)
 auths.edit=Επεξεργασία Πηγής Ταυτοποίησης
@@ -2847,6 +3009,7 @@ config.disable_router_log=Απενεργοποίηση Καταγραφής Δρ
 config.run_user=Εκτέλεση Σαν Χρήστη
 config.run_mode=Λειτουργία Εκτέλεσης
 config.git_version=Έκδοση Git
+config.app_data_path=Διαδρομή Δεδομένων Εφαρμογής
 config.repo_root_path=Ριζική Διαδρομή Αποθετηρίων
 config.lfs_root_path=Ριζική Διαδρομή LFS
 config.log_file_root_path=Διαδρομή Καταγραφών
@@ -2996,8 +3159,10 @@ monitor.queue.name=Όνομα
 monitor.queue.type=Τύπος
 monitor.queue.exemplar=Τύπος Υποδείγματος
 monitor.queue.numberworkers=Αριθμός Εργατών
+monitor.queue.activeworkers=Ενεργοί Εργάτες
 monitor.queue.maxnumberworkers=Μέγιστος Αριθμός Εργατών
 monitor.queue.numberinqueue=Πλήθος Ουράς
+monitor.queue.review_add=Εξέταση / Προσθήκη Εργατών
 monitor.queue.settings.title=Ρυθμίσεις Δεξαμενής
 monitor.queue.settings.desc=Οι δεξαμενές αυξάνονται δυναμικά όταν υπάρχει φραγή της ουράς των εργατών τους.
 monitor.queue.settings.maxnumberworkers=Μέγιστος Αριθμός Εργατών
@@ -3023,6 +3188,7 @@ notices.desc=Περιγραφή
 notices.op=Λειτ.
 notices.delete_success=Οι ειδοποιήσεις του συστήματος έχουν διαγραφεί.
 
+
 [action]
 create_repo=δημιούργησε το αποθετήριο <a href="%s">%s</a>
 rename_repo=μετονόμασε το αποθετήριο από <code>%[1]s</code> σε <a href="%[2]s">%[3]s</a>
@@ -3118,6 +3284,7 @@ desc=Διαχείριση πακέτων μητρώου.
 empty=Δεν υπάρχουν πακέτα ακόμα.
 empty.documentation=Για περισσότερες πληροφορίες σχετικά με το μητρώο πακέτων, ανατρέξτε <a target="_blank" rel="noopener noreferrer" href="%s">στην τεκμηρίωση</a>.
 empty.repo=Μήπως ανεβάσατε ένα πακέτο, αλλά δεν εμφανίζεται εδώ; Πηγαίνετε στις <a href="%[1]s">ρυθμίσεις πακέτων</a> και συνδέστε το σε αυτό το αποθετήριο.
+registry.documentation=Για περισσότερες πληροφορίες σχετικά με το μητρώο %s, ανατρέξτε στη τεκμηρίωση <a target="_blank" rel="noopener noreferrer" href="%s"></a>.
 filter.type=Τύπος
 filter.type.all=Όλα
 filter.no_result=Το φίλτρο δεν παρήγαγε αποτελέσματα.
@@ -3203,7 +3370,11 @@ pub.install=Για να εγκαταστήσετε το πακέτο μέσω τ
 pypi.requires=Απαιτεί Python
 pypi.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το pip, εκτελέστε την ακόλουθη εντολή:
 rpm.registry=Ρυθμίστε αυτό το μητρώο από τη γραμμή εντολών:
+rpm.distros.redhat=σε διανομές βασισμένες στο RedHat
+rpm.distros.suse=σε διανομές με βάση το SUSE
 rpm.install=Για να εγκαταστήσετε το πακέτο, εκτελέστε την ακόλουθη εντολή:
+rpm.repository=Πληροφορίες Αποθετηρίου
+rpm.repository.architectures=Αρχιτεκτονικές
 rubygems.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το gem, εκτελέστε την ακόλουθη εντολή:
 rubygems.install2=ή προσθέστε το στο Gemfile:
 rubygems.dependencies.runtime=Εξαρτήσεις Εκτέλεσης
@@ -3227,14 +3398,17 @@ settings.delete.success=Το πακέτο έχει διαγραφεί.
 settings.delete.error=Αποτυχία διαγραφής του πακέτου.
 owner.settings.cargo.title=Ευρετήριο Μητρώου Cargo
 owner.settings.cargo.initialize=Αρχικοποίηση Ευρετηρίου
+owner.settings.cargo.initialize.description=Απαιτείται ένα ειδικό αποθετήριο ευρετηρίου Git για τη χρήση του μητρώου Cargo. Χρησιμοποιώντας αυτή την επιλογή θα δημιουργηθεί ξανά το αποθετήριο και θα ρυθμιστεί αυτόματα.
 owner.settings.cargo.initialize.error=Αποτυχία αρχικοποίησης ευρετηρίου Cargo: %v
 owner.settings.cargo.initialize.success=Ο ευρετήριο Cargo δημιουργήθηκε με επιτυχία.
 owner.settings.cargo.rebuild=Αναδημιουργία Ευρετηρίου
+owner.settings.cargo.rebuild.description=Η ανοικοδόμηση μπορεί να είναι χρήσιμη εάν ο δείκτης δεν είναι συγχρονισμένος με τα αποθηκευμένα πακέτα Cargo.
 owner.settings.cargo.rebuild.error=Αποτυχία αναδόμησης του ευρετηρίου Cargo: %v
 owner.settings.cargo.rebuild.success=Το ευρετήριο Cargo αναδομήθηκε με επιτυχία.
 owner.settings.cleanuprules.title=Διαχείριση Κανόνων Εκκαθάρισης
 owner.settings.cleanuprules.add=Προσθήκη Κανόνα Εκκαθάρισης
 owner.settings.cleanuprules.edit=Επεξεργασία Κανόνα Εκκαθάρισης
+owner.settings.cleanuprules.none=Δεν υπάρχουν διαθέσιμοι κανόνες εκκαθάρισης. Παρακαλούμε συμβουλευτείτε την τεκμηρίωση.
 owner.settings.cleanuprules.preview=Προεπισκόπηση Κανόνα Εκκαθάρισης
 owner.settings.cleanuprules.preview.overview=%d πακέτα έχουν προγραμματιστεί να αφαιρεθούν.
 owner.settings.cleanuprules.preview.none=Ο κανόνας εκκαθάρισης δεν ταιριάζει με κανένα πακέτο.
@@ -3253,6 +3427,7 @@ owner.settings.cleanuprules.success.update=Ο κανόνας καθαρισμο
 owner.settings.cleanuprules.success.delete=Ο κανόνας καθαρισμού διαγράφηκε.
 owner.settings.chef.title=Μητρώο Chef
 owner.settings.chef.keypair=Δημιουργία ζεύγους κλειδιών
+owner.settings.chef.keypair.description=Ένα ζεύγος κλειδιών είναι απαραίτητο για ταυτοποίηση στο μητρώο Chef. Αν έχετε δημιουργήσει ένα ζεύγος κλειδιών πριν, η δημιουργία ενός νέου ζεύγους κλειδιών θα απορρίψει το παλιό ζεύγος κλειδιών.
 
 [secrets]
 secrets=Μυστικά
@@ -3279,6 +3454,7 @@ status.waiting=Αναμονή
 status.running=Εκτελείται
 status.success=Επιτυχές
 status.failure=Αποτυχία
+status.cancelled=Ακυρώθηκε
 status.skipped=Παρακάμφθηκε
 status.blocked=Αποκλείστηκε
 
@@ -3295,6 +3471,7 @@ runners.labels=Ετικέτες
 runners.last_online=Τελευταία Ώρα Σύνδεσης
 runners.runner_title=Εκτελεστής
 runners.task_list=Πρόσφατες εργασίες στον εκτελεστή
+runners.task_list.no_tasks=Δεν υπάρχει καμία εργασία ακόμα.
 runners.task_list.run=Εκτέλεση
 runners.task_list.status=Κατάσταση
 runners.task_list.repository=Αποθετήριο
@@ -3315,16 +3492,46 @@ runners.status.idle=Αδρανής
 runners.status.active=Ενεργό
 runners.status.offline=Χωρίς Σύνδεση
 runners.version=Έκδοση
+runners.reset_registration_token=Επαναφορά διακριτικού εγγραφής
 runners.reset_registration_token_success=Επιτυχής επανέκδοση διακριτικού εγγραφής του εκτελεστή
 
 runs.all_workflows=Όλες Οι Ροές Εργασίας
 runs.commit=Υποβολή
+runs.scheduled=Προγραμματισμένα
+runs.pushed_by=ωθήθηκε από
 runs.invalid_workflow_helper=Το αρχείο ροής εργασίας δεν είναι έγκυρο. Ελέγξτε το αρχείο σας: %s
+runs.no_matching_online_runner_helper=Κανένας δικτυακός δρομέας με ετικέτα: %s
+runs.actor=Φορέας
 runs.status=Κατάσταση
+runs.actors_no_select=Όλοι οι φορείς
+runs.status_no_select=Όλες οι καταστάσεις
+runs.no_results=Δεν βρέθηκαν αποτελέσματα.
+runs.no_workflows=Δεν υπάρχουν ροές εργασίας ακόμα.
+runs.no_runs=Η ροή εργασίας δεν έχει τρέξει ακόμα.
+runs.empty_commit_message=(κενό μήνυμα υποβολής)
 
+workflow.disable=Απενεργοποίηση Ροής Εργασιών
+workflow.disable_success=Η ροή εργασίας '%s' απενεργοποιήθηκε επιτυχώς.
+workflow.enable=Ενεργοποίηση Ροής Εργασίας
+workflow.enable_success=Η ροή εργασίας '%s' ενεργοποιήθηκε επιτυχώς.
+workflow.disabled=Η ροή εργασιών είναι απενεργοποιημένη.
 
 need_approval_desc=Πρέπει να εγκριθεί η εκτέλεση ροών εργασίας για pull request από fork.
 
+variables=Μεταβλητές
+variables.management=Διαχείριση Μεταβλητών
+variables.creation=Προσθήκη Μεταβλητής
+variables.none=Δεν υπάρχουν μεταβλητές ακόμα.
+variables.deletion=Αφαίρεση μεταβλητής
+variables.deletion.description=Η αφαίρεση μιας μεταβλητής είναι μόνιμη και δεν μπορεί να αναιρεθεί. Συνέχεια;
+variables.description=Η μεταβλητές θα δίνονται σε ορισμένες δράσεις και δεν μπορούν να διαβαστούν αλλιώς.
+variables.edit=Επεξεργασία Μεταβλητής
+variables.deletion.failed=Αποτυχία αφαίρεσης της μεταβλητής.
+variables.deletion.success=Η μεταβλητή έχει αφαιρεθεί.
+variables.creation.failed=Αποτυχία προσθήκης μεταβλητής.
+variables.creation.success=Η μεταβλητή "%s" έχει προστεθεί.
+variables.update.failed=Αποτυχία επεξεργασίας μεταβλητής.
+variables.update.success=Η μεταβλητή έχει τροποποιηθεί.
 
 [projects]
 type-1.display_name=Ατομικό Έργο
@@ -3332,6 +3539,11 @@ type-2.display_name=Έργο Αποθετηρίου
 type-3.display_name=Έργο Οργανισμού
 
 [git.filemode]
+changed_filemode=%[1]s → %[2]s
 ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Φάκελος
+normal_file=Κανονικό αρχείο
+executable_file=Εκτελέσιμο αρχείο
 symbolic_link=Symbolic link
+submodule=Υπομονάδα
 
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 9af4d70171..0a3d12d7a4 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -25,6 +25,7 @@ enable_javascript = This website requires JavaScript.
 toc = Table of Contents
 licenses = Licenses
 return_to_gitea = Return to Gitea
+more_items = More items
 
 username = Username
 email = Email Address
@@ -113,6 +114,7 @@ loading = Loading…
 error = Error
 error404 = The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it.
 go_back = Go Back
+invalid_data = Invalid data: %v
 
 never = Never
 unknown = Unknown
@@ -123,6 +125,7 @@ pin = Pin
 unpin = Unpin
 
 artifacts = Artifacts
+confirm_delete_artifact = Are you sure you want to delete the artifact '%s' ?
 
 archived = Archived
 
@@ -141,6 +144,43 @@ confirm_delete_selected = Confirm to delete all selected items?
 name = Name
 value = Value
 
+filter = Filter
+filter.clear = Clear Filter
+filter.is_archived = Archived
+filter.not_archived = Not Archived
+filter.is_fork = Forked
+filter.not_fork = Not Forked
+filter.is_mirror = Mirrored
+filter.not_mirror = Not Mirrored
+filter.is_template = Template
+filter.not_template = Not Template
+filter.public = Public
+filter.private = Private
+
+no_results_found = No results found.
+
+[search]
+search = Search...
+type_tooltip = Search type
+fuzzy = Fuzzy
+fuzzy_tooltip = Include results that also match the search term closely
+match = Match
+match_tooltip = Include only results that match the exact search term
+repo_kind = Search repos...
+user_kind = Search users...
+org_kind = Search orgs...
+team_kind = Search teams...
+code_kind = Search code...
+code_search_unavailable = Code search is currently not available. Please contact the site administrator.
+code_search_by_git_grep = Current code search results are provided by "git grep". There might be better results if site administrator enables Repository Indexer.
+package_kind = Search packages...
+project_kind = Search projects...
+branch_kind = Search branches...
+commit_kind = Search commits...
+runner_kind = Search runners...
+no_results = No matching results found.
+keyword_search_unavailable = Searching by keyword is currently not available. Please contact the site administrator.
+
 [aria]
 navbar = Navigation Bar
 footer = Footer
@@ -246,6 +286,7 @@ email_title = Email Settings
 smtp_addr = SMTP Host
 smtp_port = SMTP Port
 smtp_from = Send Email As
+smtp_from_invalid = The "Send Email As" address is invalid
 smtp_from_helper = Email address Gitea will use. Enter a plain email address or use the "Name" <email@example.com> format.
 mailer_user = SMTP Username
 mailer_password = SMTP Password
@@ -305,6 +346,7 @@ env_config_keys = Environment Configuration
 env_config_keys_prompt = The following environment variables will also be applied to your configuration file:
 
 [home]
+nav_menu = Navigation Menu
 uname_holder = Username or Email Address
 password_holder = Password
 switch_dashboard_context = Switch Dashboard Context
@@ -314,7 +356,6 @@ collaborative_repos = Collaborative Repositories
 my_orgs = My Organizations
 my_mirrors = My Mirrors
 view_home = View %s
-search_repos = Find a repository…
 filter = Other Filters
 filter_by_team_repositories = Filter by team repositories
 feed_of = Feed of "%s"
@@ -335,20 +376,8 @@ issues.in_your_repos = In your repositories
 repos = Repositories
 users = Users
 organizations = Organizations
-search = Search
 go_to = Go to
 code = Code
-search.type.tooltip = Search type
-search.fuzzy = Fuzzy
-search.fuzzy.tooltip = Include results that also matches the search term closely
-search.match = Match
-search.match.tooltip = Include only results that matches the exact search term
-code_search_unavailable = Currently code search is not available. Please contact your site administrator.
-repo_no_results = No matching repositories found.
-user_no_results = No matching users found.
-org_no_results = No matching organizations found.
-code_no_results = No source code matching your search term found.
-code_search_results = Search results for "%s"
 code_last_indexed_at = Last indexed %s
 relevant_repositories_tooltip = Repositories that are forks or that have no topic, no icon, and no description are hidden.
 relevant_repositories = Only relevant repositories are being shown, <a href="%s">show unfiltered results</a>.
@@ -366,7 +395,7 @@ forgot_password_title= Forgot Password
 forgot_password = Forgot password?
 sign_up_now = Need an account? Register now.
 sign_up_successful = Account was successfully created. Welcome!
-confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process.
+confirmation_mail_sent_prompt_ex = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. If your registration email address is incorrect, you can sign in again and change it.
 must_change_password = Update your password
 allow_password_change = Require user to change password (recommended)
 reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the account recovery process.
@@ -376,6 +405,7 @@ prohibit_login = Sign In Prohibited
 prohibit_login_desc = Your account is prohibited from signing in, please contact your site administrator.
 resent_limit_prompt = You have already requested an activation email recently. Please wait 3 minutes and try again.
 has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to resend a new one, please click on the button below.
+change_unconfirmed_mail_address = If your registration email address is incorrect, you can change it here and resend a new confirmation email.
 resend_mail = Click here to resend your activation email
 email_not_associate = The email address is not associated with any account.
 send_reset_mail = Send Account Recovery Email
@@ -556,6 +586,7 @@ team_name_been_taken = The team name is already taken.
 team_no_units_error = Allow access to at least one repository section.
 email_been_used = The email address is already used.
 email_invalid = The email address is invalid.
+email_domain_is_not_allowed = The domain of user email <b>%s</b> conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST. Please ensure your operation is expected.
 openid_been_used = The OpenID address "%s" is already used.
 username_password_incorrect = Username or password is incorrect.
 password_complexity = Password does not pass complexity requirements:
@@ -567,6 +598,8 @@ enterred_invalid_repo_name = The repository name you entered is incorrect.
 enterred_invalid_org_name = The organization name you entered is incorrect.
 enterred_invalid_owner_name = The new owner name is not valid.
 enterred_invalid_password = The password you entered is incorrect.
+unset_password = The login user has not set the password.
+unsupported_login_type = The login type is not supported to delete account.
 user_not_exist = The user does not exist.
 team_not_exist = The team does not exist.
 last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner for an organization.
@@ -616,6 +649,30 @@ form.name_reserved = The username "%s" is reserved.
 form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username.
 form.name_chars_not_allowed = User name "%s" contains invalid characters.
 
+block.block = Block
+block.block.user = Block user
+block.block.org = Block user for organization
+block.block.failure = Failed to block user: %s
+block.unblock = Unblock
+block.unblock.failure = Failed to unblock user: %s
+block.blocked = You have blocked this user.
+block.title = Block a user
+block.info = Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
+block.info_1 = Blocking a user prevents the following actions on your account and your repositories:
+block.info_2 = following your account
+block.info_3 = send you notifications by @mentioning your username
+block.info_4 = inviting you as a collaborator to their repositories
+block.info_5 = starring, forking or watching on repositories
+block.info_6 = opening and commenting on issues or pull requests
+block.info_7 = reacting on your comments in issues or pull requests
+block.user_to_block = User to block
+block.note = Note
+block.note.title = Optional note:
+block.note.info = The note is not visible to the blocked user.
+block.note.edit = Edit note
+block.list = Blocked users
+block.list.none = You have not blocked any users.
+
 [settings]
 profile = Profile
 account = Account
@@ -760,7 +817,6 @@ gpg_invalid_token_signature = The provided GPG key, signature and token do not m
 gpg_token_required = You must provide a signature for the below token
 gpg_token = Token
 gpg_token_help = You can generate a signature using:
-gpg_token_code = echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature = Armored GPG signature
 key_signature_gpg_placeholder = Begins with '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success = GPG key "%s" has been verified.
@@ -953,8 +1009,9 @@ fork_visibility_helper = The visibility of a forked repository cannot be changed
 fork_branch = Branch to be cloned to the fork
 all_branches = All branches
 fork_no_valid_owners = This repository can not be forked because there are no valid owners.
+fork.blocked_user = Cannot fork the repository because you are blocked by the repository owner.
 use_template = Use this template
-clone_in_vsc = Clone in VS Code
+open_with_editor = Open with %s
 download_zip = Download ZIP
 download_tar = Download TAR.GZ
 download_bundle = Download BUNDLE
@@ -1007,6 +1064,7 @@ watchers = Watchers
 stargazers = Stargazers
 stars_remove_warning = This will remove all stars from this repository.
 forks = Forks
+stars = Stars
 reactions_more = and %d more
 unit_disabled = The site administrator has disabled this repository section.
 language_other = Other
@@ -1128,6 +1186,7 @@ watch = Watch
 unstar = Unstar
 star = Star
 fork = Fork
+action.blocked_user = Cannot perform action because you are blocked by the repository owner.
 download_archive = Download Repository
 more_operations = More Operations
 
@@ -1174,6 +1233,8 @@ file_view_rendered = View Rendered
 file_view_raw = View Raw
 file_permalink = Permalink
 file_too_large = The file is too large to be shown.
+code_preview_line_from_to = Lines %[1]d to %[2]d in %[3]s
+code_preview_line_in = Line %[1]d in %[2]s
 invisible_runes_header = `This file contains invisible Unicode characters`
 invisible_runes_description = `This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.`
 ambiguous_runes_header = `This file contains ambiguous Unicode characters`
@@ -1256,6 +1317,8 @@ editor.file_editing_no_longer_exists = The file being edited, "%s", no longer ex
 editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository.
 editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them.
 editor.file_already_exists = A file named "%s" already exists in this repository.
+editor.commit_id_not_matching = The Commit ID does not match the ID when you began editing.  Commit into a patch branch and then merge.
+editor.push_out_of_date = The push appears to be out of date.
 editor.commit_empty_file_header = Commit an empty file
 editor.commit_empty_file_text = The file you're about to commit is empty. Proceed?
 editor.no_changes_to_show = There are no changes to show.
@@ -1279,9 +1342,8 @@ commits.desc = Browse source code change history.
 commits.commits = Commits
 commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.
 commits.nothing_to_compare = These branches are equal.
-commits.search = Search commits…
 commits.search.tooltip = You can prefix keywords with "author:", "committer:", "after:", or "before:", e.g. "revert author:Alice before:2019-01-13".
-commits.find = Search
+commits.search_branch = This Branch
 commits.search_all = All Branches
 commits.author = Author
 commits.message = Message
@@ -1332,7 +1394,6 @@ projects.type.basic_kanban = "Basic Kanban"
 projects.type.bug_triage = "Bug Triage"
 projects.template.desc = "Template"
 projects.template.desc_helper = "Select a project template to get started"
-projects.type.uncategorized = Uncategorized
 projects.column.edit = "Edit Column"
 projects.column.edit_title = "Name"
 projects.column.new_title = "Name"
@@ -1340,10 +1401,8 @@ projects.column.new_submit = "Create Column"
 projects.column.new = "New Column"
 projects.column.set_default = "Set Default"
 projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls"
-projects.column.unset_default = "Unset Default"
-projects.column.unset_default_desc = "Unset this column as default"
 projects.column.delete = "Delete Column"
-projects.column.deletion_desc = "Deleting a project column moves all related issues to 'Uncategorized'. Continue?"
+projects.column.deletion_desc = "Deleting a project column moves all related issues to the default column. Continue?"
 projects.column.color = "Color"
 projects.open = Open
 projects.close = Close
@@ -1378,6 +1437,8 @@ issues.new.assignees = Assignees
 issues.new.clear_assignees = Clear assignees
 issues.new.no_assignees = No Assignees
 issues.new.no_reviewers = No reviewers
+issues.new.blocked_user = Cannot create issue because you are blocked by the repository owner.
+issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or repository owner.
 issues.choose.get_started = Get Started
 issues.choose.open_external_link = Open
 issues.choose.blank = Default
@@ -1455,7 +1516,6 @@ issues.filter_sort.moststars = Most stars
 issues.filter_sort.feweststars = Fewest stars
 issues.filter_sort.mostforks = Most forks
 issues.filter_sort.fewestforks = Fewest forks
-issues.keyword_search_unavailable = Searching by keyword is currently not available. Please contact your site administrator.
 issues.action_open = Open
 issues.action_close = Close
 issues.action_label = Label
@@ -1493,6 +1553,7 @@ issues.close_comment_issue = Comment and Close
 issues.reopen_issue = Reopen
 issues.reopen_comment_issue = Comment and Reopen
 issues.create_comment = Comment
+issues.comment.blocked_user = Cannot create or edit comment because you are blocked by the poster or repository owner.
 issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@@ -1691,6 +1752,7 @@ compare.compare_head = compare
 
 pulls.desc = Enable pull requests and code reviews.
 pulls.new = New Pull Request
+pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner.
 pulls.view = View Pull Request
 pulls.compare_changes = New Pull Request
 pulls.allow_edits_from_maintainers = Allow edits from maintainers
@@ -1707,7 +1769,6 @@ pulls.compare_compare = pull from
 pulls.switch_comparison_type = Switch comparison type
 pulls.switch_head_and_base = Switch head and base
 pulls.filter_branch = Filter branch
-pulls.no_results = No results found.
 pulls.show_all_commits = Show all commits
 pulls.show_changes_since_your_last_review = Show changes since your last review
 pulls.showing_only_single_commit = Showing only changes of commit %[1]s
@@ -1775,6 +1836,7 @@ pulls.merge_pull_request = Create merge commit
 pulls.rebase_merge_pull_request = Rebase then fast-forward
 pulls.rebase_merge_commit_pull_request = Rebase then create merge commit
 pulls.squash_merge_pull_request = Create squash commit
+pulls.fast_forward_only_merge_pull_request = Fast-forward only
 pulls.merge_manually = Manually merged
 pulls.merge_commit_id = The merge commit ID
 pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed
@@ -1788,9 +1850,9 @@ pulls.unrelated_histories = Merge Failed: The merge head and base do not share a
 pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again.
 pulls.head_out_of_date = Merge Failed: Whilst generating the merge, the head was updated. Hint: Try again.
 pulls.has_merged = Failed: The pull request has been merged, you cannot merge again or change the target branch.
-pulls.push_rejected = Merge Failed: The push was rejected. Review the Git Hooks for this repository.
+pulls.push_rejected = Push Failed: The push was rejected. Review the Git Hooks for this repository.
 pulls.push_rejected_summary = Full Rejection Message
-pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.<br>Review the Git Hooks for this repository
+pulls.push_rejected_no_message = Push Failed: The push was rejected but there was no remote message. Review the Git Hooks for this repository
 pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.`
 pulls.status_checking = Some checks are pending
 pulls.status_checks_success = All checks were successful
@@ -1911,6 +1973,10 @@ wiki.page_name_desc = Enter a name for this Wiki page. Some special names are: '
 wiki.original_git_entry_tooltip = View original Git file instead of using friendly link.
 
 activity = Activity
+activity.navbar.pulse = Pulse
+activity.navbar.code_frequency = Code Frequency
+activity.navbar.contributors = Contributors
+activity.navbar.recent_commits = Recent Commits
 activity.period.filter_label = Period:
 activity.period.daily = 1 day
 activity.period.halfweekly = 3 days
@@ -1976,16 +2042,10 @@ activity.git_stats_and_deletions = and
 activity.git_stats_deletion_1 = %d deletion
 activity.git_stats_deletion_n = %d deletions
 
-search = Search
-search.search_repo = Search repository
-search.type.tooltip = Search type
-search.fuzzy = Fuzzy
-search.fuzzy.tooltip = Include results that also matches the search term closely
-search.match = Match
-search.match.tooltip = Include only results that matches the exact search term
-search.results = Search results for "%s" in <a href="%s">%s</a>
-search.code_no_results = No source code matching your search term found.
-search.code_search_unavailable = Currently code search is not available. Please contact your site administrator.
+contributors.contribution_type.filter_label = Contribution type:
+contributors.contribution_type.commits = Commits
+contributors.contribution_type.additions = Additions
+contributors.contribution_type.deletions = Deletions
 
 settings = Settings
 settings.desc = Settings is where you can manage the settings for the repository
@@ -2035,6 +2095,8 @@ settings.branches.add_new_rule = Add New Rule
 settings.advanced_settings = Advanced Settings
 settings.wiki_desc = Enable Repository Wiki
 settings.use_internal_wiki = Use Built-In Wiki
+settings.default_wiki_branch_name = Default Wiki Branch Name
+settings.failed_to_change_default_wiki_branch = Failed to change the default wiki branch.
 settings.use_external_wiki = Use External Wiki
 settings.external_wiki_url = External Wiki URL
 settings.external_wiki_url_error = The external wiki URL is not a valid URL.
@@ -2064,7 +2126,11 @@ settings.pulls.default_delete_branch_after_merge = Delete pull request branch af
 settings.pulls.default_allow_edits_from_maintainers = Allow edits from maintainers by default
 settings.releases_desc = Enable Repository Releases
 settings.packages_desc = Enable Repository Packages Registry
-settings.projects_desc = Enable Repository Projects
+settings.projects_desc = Enable Projects
+settings.projects_mode_desc = Projects Mode (which kinds of projects to show)
+settings.projects_mode_repo = Repo projects only
+settings.projects_mode_owner = Only user or org projects
+settings.projects_mode_all = All projects
 settings.actions_desc = Enable Repository Actions
 settings.admin_settings = Administrator Settings
 settings.admin_enable_health_check = Enable Repository Health Checks (git fsck)
@@ -2090,6 +2156,7 @@ settings.convert_fork_succeed = The fork has been converted into a regular repos
 settings.transfer = Transfer Ownership
 settings.transfer.rejected = Repository transfer was rejected.
 settings.transfer.success = Repository transfer was successful.
+settings.transfer.blocked_user = Cannot transfer repository because you are blocked by the new owner.
 settings.transfer_abort = Cancel transfer
 settings.transfer_abort_invalid = You cannot cancel a non existent repository transfer.
 settings.transfer_abort_success = The repository transfer to %s was successfully canceled.
@@ -2135,11 +2202,11 @@ settings.add_collaborator_success = The collaborator has been added.
 settings.add_collaborator_inactive_user = Cannot add an inactive user as a collaborator.
 settings.add_collaborator_owner = Cannot add an owner as a collaborator.
 settings.add_collaborator_duplicate = The collaborator is already added to this repository.
+settings.add_collaborator.blocked_user = The collaborator is blocked by the repository owner or vice versa.
 settings.delete_collaborator = Remove
 settings.collaborator_deletion = Remove Collaborator
 settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this repository. Continue?
 settings.remove_collaborator_success = The collaborator has been removed.
-settings.search_user_placeholder = Search user…
 settings.org_not_allowed_to_be_collaborator = Organizations cannot be added as a collaborator.
 settings.change_team_access_not_allowed = Changing team access for repository has been restricted to organization owner
 settings.team_not_in_organization = The team is not in the same organization as the repository
@@ -2147,7 +2214,6 @@ settings.teams = Teams
 settings.add_team = Add Team
 settings.add_team_duplicate = Team already has the repository
 settings.add_team_success = The team now have access to the repository.
-settings.search_team = Search Team…
 settings.change_team_permission_tip = Team's permission is set on the team setting page and can't be changed per repository
 settings.delete_team_tip = This team has access to all repositories and can't be removed
 settings.remove_team_success = The team's access to the repository has been removed.
@@ -2300,9 +2366,7 @@ settings.protect_whitelist_committers = Whitelist Restricted Push
 settings.protect_whitelist_committers_desc = Only whitelisted users or teams will be allowed to push to this branch (but not force push).
 settings.protect_whitelist_deploy_keys = Whitelist deploy keys with write access to push.
 settings.protect_whitelist_users = Whitelisted users for pushing:
-settings.protect_whitelist_search_users = Search users…
 settings.protect_whitelist_teams = Whitelisted teams for pushing:
-settings.protect_whitelist_search_teams = Search teams…
 settings.protect_merge_whitelist_committers = Enable Merge Whitelist
 settings.protect_merge_whitelist_committers_desc = Allow only whitelisted users or teams to merge pull requests into this branch.
 settings.protect_merge_whitelist_users = Whitelisted users for merging:
@@ -2547,7 +2611,6 @@ branch.default_deletion_failed = Branch "%s" is the default branch. It cannot be
 branch.restore = Restore Branch "%s"
 branch.download = Download Branch "%s"
 branch.rename = Rename Branch "%s"
-branch.search = Search Branch
 branch.included_desc = This branch is part of the default branch
 branch.included = Included
 branch.create_new_branch = Create branch from branch:
@@ -2578,6 +2641,16 @@ find_file.no_matching = No matching file found
 error.csv.too_large = Can't render this file because it is too large.
 error.csv.unexpected = Can't render this file because it contains an unexpected character in line %d and column %d.
 error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d.
+error.broken_git_hook = Git hooks of this repository seem to be broken. Please follow the <a target="_blank" rel="noreferrer" href="%s">documentation</a> to fix them, then push some commits to refresh the status.
+
+[graphs]
+component_loading = Loading %s...
+component_loading_failed = Could not load %s
+component_loading_info = This might take a bit…
+component_failed_to_load = An unexpected error happened.
+code_frequency.what = code frequency
+contributors.what = contributions
+recent_commits.what = recent commits
 
 [org]
 org_name_holder = Organization Name
@@ -2683,7 +2756,6 @@ teams.write_permission_desc = This team grants <strong>Write</strong> access: me
 teams.admin_permission_desc = This team grants <strong>Admin</strong> access: members can read from, push to and add collaborators to team repositories.
 teams.create_repo_permission_desc = Additionally, this team grants <strong>Create repository</strong> permission: members can create new repositories in organization.
 teams.repositories = Team Repositories
-teams.search_repo_placeholder = Search repository…
 teams.remove_all_repos_title = Remove all team repositories
 teams.remove_all_repos_desc = This will remove all repositories from the team.
 teams.add_all_repos_title = Add all repositories
@@ -2692,6 +2764,7 @@ teams.add_nonexistent_repo = "The repository you're trying to add doesn't exist,
 teams.add_duplicate_users = User is already a team member.
 teams.repos.none = No repositories could be accessed by this team.
 teams.members.none = No members on this team.
+teams.members.blocked_user = Cannot add the user because it is blocked by the organization.
 teams.specific_repositories = Specific repositories
 teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>.
 teams.all_repositories = All repositories
@@ -2704,6 +2777,7 @@ teams.invite.by = Invited by %s
 teams.invite.description = Please click the button below to join the team.
 
 [admin]
+maintenance = Maintenance
 dashboard = Dashboard
 self_check = Self Check
 identity_access = Identity & Access
@@ -2716,6 +2790,8 @@ integrations = Integrations
 authentication = Authentication Sources
 emails = User Emails
 config = Configuration
+config_summary = Summary
+config_settings = Settings
 notices = System Notices
 monitor = Monitoring
 first_page = First
@@ -2725,7 +2801,7 @@ settings = Admin Settings
 
 dashboard.new_version_hint = Gitea %s is now available, you are running %s. Check <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">the blog</a> for more details.
 dashboard.statistic = Summary
-dashboard.operations = Maintenance Operations
+dashboard.maintenance_operations = Maintenance Operations
 dashboard.system_status = System Status
 dashboard.operation_name = Operation Name
 dashboard.operation_switch = Switch
@@ -2892,9 +2968,6 @@ repos.unadopted.no_more = No more unadopted repositories found
 repos.owner = Owner
 repos.name = Name
 repos.private = Private
-repos.watches = Watches
-repos.stars = Stars
-repos.forks = Forks
 repos.issues = Issues
 repos.size = Size
 repos.lfs_size = LFS Size
@@ -3019,7 +3092,7 @@ auths.tip.nextcloud = Register a new OAuth consumer on your instance using the f
 auths.tip.dropbox = Create a new application at https://www.dropbox.com/developers/apps
 auths.tip.facebook = Register a new application at https://developers.facebook.com/apps and add the product "Facebook Login"
 auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new
-auths.tip.gitlab = Register a new application on https://gitlab.com/profile/applications
+auths.tip.gitlab_new = Register a new application on https://gitlab.com/-/profile/applications
 auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console at https://console.developers.google.com/
 auths.tip.openid_connect = Use the OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) to specify the endpoints
 auths.tip.twitter = Go to https://dev.twitter.com/apps, create an application and ensure that the “Allow this application to be used to Sign in with Twitter” option is enabled
@@ -3155,6 +3228,7 @@ config.picture_config = Picture and Avatar Configuration
 config.picture_service = Picture Service
 config.disable_gravatar = Disable Gravatar
 config.enable_federated_avatar = Enable Federated Avatars
+config.open_with_editor_app_help = The "Open with" editors for the clone menu. If left empty, the default will be used. Expand to see the default.
 
 config.git_config = Git Configuration
 config.git_disable_diff_highlight = Disable Diff Syntax Highlight
@@ -3234,6 +3308,7 @@ notices.op = Op.
 notices.delete_success = The system notices have been deleted.
 
 self_check.no_problem_found = No problem found yet.
+self_check.startup_warnings = Startup warnings:
 self_check.database_collation_mismatch = Expect database to use collation: %s
 self_check.database_collation_case_insensitive = Database is using a collation %s, which is an insensitive collation. Although Gitea could work with it, there might be some rare cases which don't work as expected.
 self_check.database_inconsistent_collation_columns = Database is using collation %s, but these columns are using mismatched collations. It might cause some unexpected problems.
@@ -3553,6 +3628,7 @@ runs.scheduled = Scheduled
 runs.pushed_by = pushed by
 runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s
 runs.no_matching_online_runner_helper = No matching online runner with label: %s
+runs.no_job_without_needs = The workflow must contain at least one job without dependencies.
 runs.actor = Actor
 runs.status = Status
 runs.actors_no_select = All actors
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index 1a82ce5b76..fc78e1d439 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -139,6 +139,15 @@ confirm_delete_selected=¿Borrar todos los elementos seleccionados?
 name=Nombre
 value=Valor
 
+filter=Filtro
+filter.is_archived=Archivado
+filter.is_template=Plantilla
+filter.public=Público
+filter.private=Privado
+
+
+[search]
+
 [aria]
 navbar=Barra de navegación
 footer=Pie
@@ -312,7 +321,6 @@ collaborative_repos=Repositorios colaborativos
 my_orgs=Mis organizaciones
 my_mirrors=Mis réplicas
 view_home=Ver %s
-search_repos=Buscar un repositorio…
 filter=Otros filtros
 filter_by_team_repositories=Filtrar por repositorios de equipo
 feed_of=`Suministro de noticias de "%s"`
@@ -333,20 +341,8 @@ issues.in_your_repos=En tus repositorios
 repos=Repositorios
 users=Usuarios
 organizations=Organizaciones
-search=Buscar
 go_to=Ir a
 code=Código
-search.type.tooltip=Tipo de búsqueda
-search.fuzzy=Parcial
-search.fuzzy.tooltip=Incluye los resultados que también coincidan con el término de búsqueda
-search.match=Coincidir
-search.match.tooltip=Incluye sólo los resultados que coincidan con el término de búsqueda exacto
-code_search_unavailable=Actualmente la búsqueda de código no está disponible. Póngase en contacto con el administrador de su sitio.
-repo_no_results=No se ha encontrado ningún repositorio coincidente.
-user_no_results=No se ha encontrado ningún usuario coincidente.
-org_no_results=No se ha encontrado ninguna organización coincidente.
-code_no_results=No se ha encontrado código de fuente que coincida con su término de búsqueda.
-code_search_results=Resultados de búsqueda para «%s»
 code_last_indexed_at=Indexado por última vez %s
 relevant_repositories_tooltip=Repositorios que son bifurcaciones o que no tienen ningún tema, ningún icono, y ninguna descripción están ocultos.
 relevant_repositories=Solo se muestran repositorios relevantes, <a href="%s">mostrar resultados sin filtrar</a>.
@@ -363,7 +359,6 @@ forgot_password_title=He olvidado mi contraseña
 forgot_password=¿Has olvidado tu contraseña?
 sign_up_now=¿Necesitas una cuenta? Regístrate ahora.
 sign_up_successful=La cuenta se ha creado correctamente. ¡Bienvenido!
-confirmation_mail_sent_prompt=Un nuevo correo de confirmación se ha enviado a <b>%s</b>. Comprueba tu bandeja de entrada en las siguientes %s para completar el registro.
 must_change_password=Actualizar su contraseña
 allow_password_change=Obligar al usuario a cambiar la contraseña (recomendado)
 reset_password_mail_sent_prompt=Un correo de confirmación se ha enviado a <b>%s</b>. Compruebe su bandeja de entrada en las siguientes %s para completar el proceso de recuperación de la cuenta.
@@ -585,6 +580,7 @@ org_still_own_packages=Esta organización todavía posee uno o más paquetes, el
 
 target_branch_not_exist=La rama de destino no existe
 
+
 [user]
 change_avatar=Cambiar su avatar…
 joined_on=Se unió el %s
@@ -610,6 +606,7 @@ form.name_reserved=El nombre de usuario "%s" está reservado.
 form.name_pattern_not_allowed=El patrón "%s" no está permitido en un nombre de usuario.
 form.name_chars_not_allowed=El nombre de usuario "%s" contiene caracteres no válidos.
 
+
 [settings]
 profile=Perfil
 account=Cuenta
@@ -754,7 +751,6 @@ gpg_invalid_token_signature=La clave GPG proporcionada, la firma y el token no c
 gpg_token_required=Debe proporcionar una firma para el token de abajo
 gpg_token=Token
 gpg_token_help=Puede generar una firma de la siguiente manera:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Firma GPG armadura
 key_signature_gpg_placeholder=Comienza con '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=La clave GPG "%s" ha sido verificada.
@@ -944,7 +940,6 @@ fork_branch=Rama a clonar en la bifurcación
 all_branches=Todas las ramas
 fork_no_valid_owners=Este repositorio no puede ser bifurcado porque no hay propietarios válidos.
 use_template=Utilizar esta plantilla
-clone_in_vsc=Clonar en VS Code
 download_zip=Descargar ZIP
 download_tar=Descargar TAR.GZ
 download_bundle=Descargar BUNDLE
@@ -1263,9 +1258,7 @@ commits.desc=Ver el historial de cambios de código fuente.
 commits.commits=Commits
 commits.no_commits=No hay commits en común. "%s" y "%s" tienen historias totalmente diferentes.
 commits.nothing_to_compare=Estas ramas son iguales.
-commits.search=Buscar commits…
 commits.search.tooltip=Puede prefijar palabras clave con "author:", "committer:", "after:", o "before:", p. ej., "revertir author:Alice before:2019-01-13".
-commits.find=Buscar
 commits.search_all=Todas las Ramas
 commits.author=Autor
 commits.message=Mensaje
@@ -1316,7 +1309,6 @@ projects.type.basic_kanban=Kanban básico
 projects.type.bug_triage=Prueba de error
 projects.template.desc=Plantilla del proyecto
 projects.template.desc_helper=Seleccione una plantilla de proyecto para empezar
-projects.type.uncategorized=Sin categorizar
 projects.column.edit=Editar columna
 projects.column.edit_title=Nombre
 projects.column.new_title=Nombre
@@ -1324,10 +1316,7 @@ projects.column.new_submit=Crear columna
 projects.column.new=Nueva columna
 projects.column.set_default=Establecer como predeterminado
 projects.column.set_default_desc=Establecer esta columna como predeterminada para incidencias no categorizadas y pulls
-projects.column.unset_default=Anular valor predeterminado
-projects.column.unset_default_desc=Anular esta columna como la predeterminada
 projects.column.delete=Borrar columna
-projects.column.deletion_desc=Eliminar una columna del proyecto mueve todos los problemas relacionados a 'Sin categorizar'. ¿Continuar?
 projects.column.color=Color
 projects.open=Abrir
 projects.close=Cerrar
@@ -1439,7 +1428,6 @@ issues.filter_sort.moststars=Mas estrellas
 issues.filter_sort.feweststars=Menor número de estrellas
 issues.filter_sort.mostforks=La mayoría de forks
 issues.filter_sort.fewestforks=Menor número de forks
-issues.keyword_search_unavailable=La búsqueda por palabra clave no está disponible actualmente. Por favor, contacte con el administrador de su sitio.
 issues.action_open=Abrir
 issues.action_close=Cerrar
 issues.action_label=Etiqueta
@@ -1691,7 +1679,6 @@ pulls.compare_compare=recuperar de
 pulls.switch_comparison_type=Cambiar tipo de comparación
 pulls.switch_head_and_base=Intercambiar cabeza y base
 pulls.filter_branch=Filtrar rama
-pulls.no_results=Sin resultados.
 pulls.show_all_commits=Mostrar todos los commits
 pulls.show_changes_since_your_last_review=Mostrar cambios desde tu última revisión
 pulls.showing_only_single_commit=Mostrando solo los cambios del commit %[1]s
@@ -1952,16 +1939,7 @@ activity.git_stats_and_deletions=y
 activity.git_stats_deletion_1=%d eliminación
 activity.git_stats_deletion_n=%d eliminaciones
 
-search=Buscar
-search.search_repo=Buscar repositorio
-search.type.tooltip=Tipo de búsqueda
-search.fuzzy=Parcial
-search.fuzzy.tooltip=Incluye los resultados que también coinciden aproximadamente con el término de búsqueda
-search.match=Coincidir
-search.match.tooltip=Incluye sólo los resultados que coincidan con el término de búsqueda exacto
-search.results=Resultados de la búsqueda para "%s" en <a href="%s">%s</a>
-search.code_no_results=No se ha encontrado código de fuente que coincida con su término de búsqueda.
-search.code_search_unavailable=Actualmente la búsqueda de código no está disponible. Póngase en contacto con el administrador de su sitio.
+contributors.contribution_type.commits=Commits
 
 settings=Configuración
 settings.desc=La configuración es donde puede administrar la configuración del repositorio
@@ -2040,6 +2018,7 @@ settings.pulls.default_allow_edits_from_maintainers=Permitir ediciones de manten
 settings.releases_desc=Activar lanzamientos del repositorio
 settings.packages_desc=Habilitar registro de paquetes de repositorio
 settings.projects_desc=Activar Proyectos de Repositorio
+settings.projects_mode_all=Todos los proyectos
 settings.actions_desc=Activar Acciones del repositorio
 settings.admin_settings=Ajustes de administrador
 settings.admin_enable_health_check=Activar cheques de estado de salud del repositorio (git fsck)
@@ -2114,7 +2093,6 @@ settings.delete_collaborator=Eliminar
 settings.collaborator_deletion=Eliminar colaborador
 settings.collaborator_deletion_desc=Eliminar un colaborador revocará su acceso a este repositorio. ¿Continuar?
 settings.remove_collaborator_success=El colaborador ha sido eliminado.
-settings.search_user_placeholder=Buscar usuario…
 settings.org_not_allowed_to_be_collaborator=Las organizaciones no pueden ser añadidas como colaboradoras.
 settings.change_team_access_not_allowed=Cambiar el acceso del equipo al repositorio se ha restringido al propietario de la organización
 settings.team_not_in_organization=El equipo no pertenece a la misma organización que el repositorio
@@ -2122,7 +2100,6 @@ settings.teams=Equipos
 settings.add_team=Añadir equipo
 settings.add_team_duplicate=El equipo ya tiene acceso al repositorio
 settings.add_team_success=Ahora el equipo ya tiene acceso al repositorio.
-settings.search_team=Buscar equipos…
 settings.change_team_permission_tip=El permiso del equipo está establecido en la página de configuración del equipo y no puede ser cambiado por repositorio
 settings.delete_team_tip=Este equipo tiene acceso a todos los repositorios y no puede ser eliminado
 settings.remove_team_success=Se ha eliminado el acceso del equipo al repositorio.
@@ -2275,9 +2252,7 @@ settings.protect_whitelist_committers=Hacer push restringido a la lista blanca
 settings.protect_whitelist_committers_desc=Sólo se permitirá a los usuarios o equipos de la lista blanca hacer push a esta rama (pero no forzar push).
 settings.protect_whitelist_deploy_keys=Lista blanca de claves de despliegue con acceso de escritura a push.
 settings.protect_whitelist_users=Usuarios en la lista blanca para hacer push:
-settings.protect_whitelist_search_users=Buscar usuarios…
 settings.protect_whitelist_teams=Equipos en la lista blanca para hacer push:
-settings.protect_whitelist_search_teams=Buscar equipos…
 settings.protect_merge_whitelist_committers=Activar lista blanca para fusionar
 settings.protect_merge_whitelist_committers_desc=Permitir a los usuarios o equipos de la lista a fusionar peticiones pull dentro de esta rama.
 settings.protect_merge_whitelist_users=Usuarios en la lista blanca para fusionar:
@@ -2518,7 +2493,6 @@ branch.default_deletion_failed=La rama "%s" es la rama por defecto. No se puede
 branch.restore=`Restaurar rama "%s"`
 branch.download=`Descargar rama "%s"`
 branch.rename=`Renombrar rama "%s"`
-branch.search=Buscar rama
 branch.included_desc=Esta rama forma parte de la predeterminada
 branch.included=Incluida
 branch.create_new_branch=Crear rama desde la rama:
@@ -2550,6 +2524,8 @@ error.csv.too_large=No se puede renderizar este archivo porque es demasiado gran
 error.csv.unexpected=No se puede procesar este archivo porque contiene un carácter inesperado en la línea %d y la columna %d.
 error.csv.invalid_field_count=No se puede procesar este archivo porque tiene un número incorrecto de campos en la línea %d.
 
+[graphs]
+
 [org]
 org_name_holder=Nombre de la organización
 org_full_name_holder=Nombre completo de la organización
@@ -2654,7 +2630,6 @@ teams.write_permission_desc=Este equipo tiene permisos de <strong>Escritura</str
 teams.admin_permission_desc=Este equipo tiene permisos de <strong>Administración</strong>: los miembros pueden ver, hacer push y añadir colaboradores a los repositorios del equipo.
 teams.create_repo_permission_desc=Adicionalmente, este equipo concede permiso <strong>Crear repositorio</strong>: los miembros pueden crear nuevos repositorios en la organización.
 teams.repositories=Repositorios del equipo
-teams.search_repo_placeholder=Buscar repositorio…
 teams.remove_all_repos_title=Eliminar todos los repositorios del equipo
 teams.remove_all_repos_desc=Esto eliminará todos los repositorios del equipo.
 teams.add_all_repos_title=Añadir todos los repositorios
@@ -2686,6 +2661,8 @@ integrations=Integraciones
 authentication=Orígenes de autenticación
 emails=Correos de usuario
 config=Configuración
+config_summary=Resumen
+config_settings=Configuración
 notices=Notificaciones del sistema
 monitor=Monitorización
 first_page=Primera
@@ -2695,7 +2672,6 @@ settings=Configuración de Admin
 
 dashboard.new_version_hint=Gitea %s ya está disponible, estás ejecutando %s. Revisa <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">el blog</a> para más detalles.
 dashboard.statistic=Resumen
-dashboard.operations=Operaciones de mantenimiento
 dashboard.system_status=Estado del sistema
 dashboard.operation_name=Nombre de la operación
 dashboard.operation_switch=Interruptor
@@ -2859,9 +2835,6 @@ repos.unadopted.no_more=No se encontraron más repositorios no adoptados
 repos.owner=Propietario
 repos.name=Nombre
 repos.private=Privado
-repos.watches=Vigilantes
-repos.stars=Estrellas
-repos.forks=Forks
 repos.issues=Incidencias
 repos.size=Tamaño
 repos.lfs_size=Tamaño LFS
@@ -2985,7 +2958,6 @@ auths.tip.nextcloud=`Registre un nuevo consumidor OAuth en su instancia usando e
 auths.tip.dropbox=Crear nueva aplicación en https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Registre una nueva aplicación en https://developers.facebook.com/apps y agregue el producto "Facebook Login"`
 auths.tip.github=Registre una nueva aplicación OAuth en https://github.com/settings/applications/new
-auths.tip.gitlab=Registrar nueva solicitud en https://gitlab.com/profile/applications
 auths.tip.google_plus=Obtener credenciales de cliente OAuth2 desde la consola API de Google en https://console.developers.google.com/
 auths.tip.openid_connect=Use el OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) para especificar los puntos finales
 auths.tip.twitter=Ir a https://dev.twitter.com/apps, crear una aplicación y asegurarse de que la opción "Permitir que esta aplicación sea usada para iniciar sesión con Twitter" está activada
@@ -3199,6 +3171,7 @@ notices.desc=Descripción
 notices.op=Operación
 notices.delete_success=Los avisos del sistema se han eliminado.
 
+
 [action]
 create_repo=creó el repositorio <a href="%s">%s</a>
 rename_repo=repositorio renombrado de <code>%[1]s</code> a <a href="%[2]s">%[3]s</a>
@@ -3383,6 +3356,8 @@ rpm.registry=Configurar este registro desde la línea de comandos:
 rpm.distros.redhat=en distribuciones basadas en RedHat
 rpm.distros.suse=en distribuciones basadas en SUSE
 rpm.install=Para instalar el paquete, ejecute el siguiente comando:
+rpm.repository=Información del repositorio
+rpm.repository.architectures=Arquitecturas
 rubygems.install=Para instalar el paquete usando gem, ejecute el siguiente comando:
 rubygems.install2=o añádelo al archivo Gemfile:
 rubygems.dependencies.runtime=Dependencias en tiempo de ejecución
@@ -3530,7 +3505,6 @@ variables.none=Aún no hay variables.
 variables.deletion=Eliminar variable
 variables.deletion.description=Eliminar una variable es permanente y no se puede deshacer. ¿Continuar?
 variables.description=Las variables se pasarán a ciertas acciones y no se podrán leer de otro modo.
-variables.id_not_exist=Variable con id %d no existe.
 variables.edit=Editar variable
 variables.deletion.failed=No se pudo eliminar la variable.
 variables.deletion.success=La variable ha sido eliminada.
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index c9099299a0..d19eb356d2 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -100,6 +100,15 @@ concept_user_organization=سازمان
 
 name=نام
 
+filter=فیلتر
+filter.is_archived=بایگانی شده
+filter.is_template=قالب
+filter.public=عمومی
+filter.private=خصوصی
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -233,7 +242,6 @@ collaborative_repos=مخازن همکاری
 my_orgs=سازمان های من
 my_mirrors=قرینه‌های من
 view_home=نمایش %s
-search_repos=یافتن مخزن…
 filter=فیلترهای دیگر
 filter_by_team_repositories=فیلتر کردن با مخازن تیم‌ها
 feed_of=`خوراک از "%s"`
@@ -254,14 +262,7 @@ issues.in_your_repos=در مخازن شما
 repos=مخازن
 users=کاربران
 organizations=سازمان ها
-search=جستجو
 code=کد
-search.fuzzy=نادقیق
-search.match=تطابق
-repo_no_results=مخزنی مطابق با این مورد یافت نشد.
-user_no_results=کاربری مطابق با این مورد یافت نشد.
-org_no_results=سازمانی مطابق با این مورد یافت نشد.
-code_no_results=کد منبعی مطابق با جستجوی شما یافت نشد.
 code_last_indexed_at=آخرین به روزرسانی در %s
 
 [auth]
@@ -274,7 +275,6 @@ remember_me=این دستگاه را بخاطر بسپار
 forgot_password_title=گذرواژه خود را فراموش کرده ام
 forgot_password=گذرواژه خود را فراموش کرده‌اید؟
 sign_up_now=نیاز به یک حساب دارید؟ هم‌اکنون ثبت نام کنید.
-confirmation_mail_sent_prompt=ایمیل تاییدیه جدیدی به <b>%s</b> ارسال شد. لطفا صندوق ورودی خود را در %d ساعت آینده برای تکمیل فرایند ثبت نام بررسی کنید.
 must_change_password=گذرواژه خود را به روز کنید
 allow_password_change=نیاز به کاربر برای تغییرگذرواژه (توصیه می شود)
 reset_password_mail_sent_prompt=ایمیل تاییدیه جدیدی به <b>%s</b> ارسال شد. لطفا صندوق ورودی خود را در %s آینده برای فرآیند بازیابی حساب کاربری خود بررسی کنید.
@@ -463,6 +463,7 @@ auth_failed=تشخیص هویت ناموفق: %v
 
 target_branch_not_exist=شاخه مورد نظر وجود ندارد.
 
+
 [user]
 change_avatar=تغییر آواتار…
 repositories=مخازن
@@ -479,6 +480,7 @@ user_bio=زندگی‌نامه
 disabled_public_activity=این کاربر نمایش عمومی فعالیت های خود را غیرفعال کرده است.
 
 
+
 [settings]
 profile=نمایه
 account=حساب کاربری
@@ -590,7 +592,6 @@ gpg_invalid_token_signature=کلید GPG ارائه شده، امضا و ژتو
 gpg_token_required=باید یک امضا برای ژتون زیر ارائه کنید
 gpg_token=توکن
 gpg_token_help=با این میتوانید یک امضاء بسازید:
-gpg_token_code=‪echo "%s" | gpg -a --default-key %s --detach-sig‬
 gpg_token_signature=امضای GPG زره‌پوش
 key_signature_gpg_placeholder=با '-----BEGIN PGP SIGNATURE-----' شروع می‌شود
 ssh_key_verified=کلید تأیید شده
@@ -729,7 +730,6 @@ fork_repo=انشعاب از مخزن
 fork_from=انشعاب از
 fork_visibility_helper=نمایان بودن مخزن منشعب شده غیر قابل تغییر است.
 use_template=استفاده از این الگو
-clone_in_vsc=کلون کردن در VS Code
 download_zip=دانلود ZIP
 download_tar=دانلود TAR.GZ
 download_bundle=بارگیری باندل
@@ -971,8 +971,6 @@ editor.require_signed_commit=شاخه یک کامیت امضا شده لازم 
 commits.desc=تاریخچه تغییرات کد منبع را مرور کنید.
 commits.commits=کامیت‌ها
 commits.nothing_to_compare=این شاخه ها برابرند.
-commits.search=جست‌وجو کامیت‌ها…
-commits.find=جستجو
 commits.search_all=همه شاخه ها
 commits.author=مولف
 commits.message=پیام
@@ -1009,7 +1007,6 @@ projects.type.basic_kanban=پایه بر اساس سیستم کانبان (یک
 projects.type.bug_triage=اشکال Triage
 projects.template.desc=قالب پروژه
 projects.template.desc_helper=برای شروع یک قالب پروژه را انتخاب کنید
-projects.type.uncategorized=دسته‌بندی نشده
 projects.column.edit_title=نام
 projects.column.new_title=نام
 projects.column.color=رنگ
@@ -1300,7 +1297,6 @@ pulls.compare_compare=واکشی از
 pulls.switch_comparison_type=سوئیچ نوع مقایسه
 pulls.switch_head_and_base=سر و پایه سوئیچ
 pulls.filter_branch=صافی شاخه
-pulls.no_results=هیچ نتیجه‌ای یافت نشد.
 pulls.nothing_to_compare=این شاخه‎ها یکی هستند. نیازی به تقاضای واکشی نیست.
 pulls.nothing_to_compare_and_allow_empty_pr=این شاخه ها برابر هستند. این PR خالی خواهد بود.
 pulls.has_pull_request=`A درخواست pull بین این شاخه ها از قبل وجود دارد: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1498,12 +1494,7 @@ activity.git_stats_and_deletions=و
 activity.git_stats_deletion_1=%d مذحوف
 activity.git_stats_deletion_n=%d مذحوف
 
-search=جستجو
-search.search_repo=جستجوی مخزن
-search.fuzzy=درهم
-search.match=مطابق
-search.results=نتیجه جستجو برای "%s" در <a href="%s">%s</a>
-search.code_no_results=کد منبعی مطابق با جستجوی شما یافت نشد.
+contributors.contribution_type.commits=کامیت‌ها
 
 settings=تنظيمات
 settings.desc=تنظیمات جایی است که شما می‌توانید تنظیمات مخزن خود را مدیریت کنید
@@ -1620,7 +1611,6 @@ settings.delete_collaborator=حذف
 settings.collaborator_deletion=حذف‌کردن همکار
 settings.collaborator_deletion_desc=حذف یک همکار از مخزن دسترسی‌های آنها را را مجدد لغو می‌کند. آیا ادامه می‌دهید؟
 settings.remove_collaborator_success=همكار حذف شد.
-settings.search_user_placeholder=جستجوی کاربر…
 settings.org_not_allowed_to_be_collaborator=سازمان ها را نمیتوان به عنوان همکار افزود.
 settings.change_team_access_not_allowed=تغییر دسترسی های تیم برای این مخزن توسط مالک ارگان محدود شده است
 settings.team_not_in_organization=تیم همانند ارگان برای این مخزن نیست
@@ -1628,7 +1618,6 @@ settings.teams=تیم ها
 settings.add_team=افزودن تیم
 settings.add_team_duplicate=تیم پیش از این مخزن داشته
 settings.add_team_success=تیم هم‌اکنون به مخزن دسترسی دارد.
-settings.search_team=جستجوی تیم…
 settings.change_team_permission_tip=دسترسی تیم در صفحه تنظیمات تیم انجام شده و برای هر مخزن نمی تواند تغییر یابد
 settings.delete_team_tip=این تیم به تمامی مخازن دسترسی دارد و نمی تواند حذف شود
 settings.remove_team_success=دسترسی تیم به مخزن حذف شد.
@@ -1745,9 +1734,7 @@ settings.protect_whitelist_committers=لیست سفید برای درج محدو
 settings.protect_whitelist_committers_desc=فقط به کاربران یا تیم‌های موجود لیست سفید برای درج در این شاخه اجازه خواهند داشت (اما نه درج اجباری).
 settings.protect_whitelist_deploy_keys=فهرست سفید کلیدهای استقرار با دسترسی نوشتن برای push کردن.
 settings.protect_whitelist_users=کاربران لیست سفید برای درج در مخزن:
-settings.protect_whitelist_search_users=جستجوی کاربر…
 settings.protect_whitelist_teams=تیم‌های لیست سفید برای درج در مخزن:
-settings.protect_whitelist_search_teams=جستجوی تیم ها…
 settings.protect_merge_whitelist_committers=فعال کردن لیست سفید ادغام
 settings.protect_merge_whitelist_committers_desc=اجازه به کاربران یا تیم‌های موجود لیست سفید برای تقاضا ادغام واکشی در این شاخه.
 settings.protect_merge_whitelist_users=کاربران لیست سفید برای ادغام:
@@ -1951,6 +1938,8 @@ error.csv.too_large=نمی توان این فایل را رندر کرد زیر
 error.csv.unexpected=نمی توان این فایل را رندر کرد زیرا حاوی یک کاراکتر غیرمنتظره در خط %d و ستون %d است.
 error.csv.invalid_field_count=نمی توان این فایل را رندر کرد زیرا تعداد فیلدهای آن در خط %d اشتباه است.
 
+[graphs]
+
 [org]
 org_name_holder=نام سازمان
 org_full_name_holder=نام کامل سازمان
@@ -2042,7 +2031,6 @@ teams.write_permission_desc=این تیم دسترسی <strong>نوشتن</stron
 teams.admin_permission_desc=این تیم دسترسی <strong>نوشتن</strong> خواهد داشت: اعضا خواهند توانست مخازن تیم را خوانده ، تغییراتی در آنها اعمال کرده و یا همکارانشان را به مخازن اضافه نمایند.
 teams.create_repo_permission_desc=علاوه بر این ، این تیم اجازه <strong> ساخت مخزن </strong> دسترسی : اعضا می توانند مخازن جدیدی را در سازمان ایجاد کنند.
 teams.repositories=مخازن تیم
-teams.search_repo_placeholder=جستجوی مخزن...
 teams.remove_all_repos_title=حذف تمام مخازن تیم
 teams.remove_all_repos_desc=با این کار همه مخازن از تیم حذف می شوند.
 teams.add_all_repos_title=افزودن همه مخازن
@@ -2067,6 +2055,8 @@ hooks=وب هوک ها
 authentication=منابع احراز هویت
 emails=ایمیل های کاربر
 config=پیکربندی
+config_summary=چکیده
+config_settings=تنظيمات
 notices=هشدارهای سامانه
 monitor=نظارت
 first_page=نخستین
@@ -2074,7 +2064,6 @@ last_page=واپسین
 total=مجموع: %d
 
 dashboard.statistic=چکیده
-dashboard.operations=عملیات‌های نگهداری
 dashboard.system_status=وضعیت سامانه
 dashboard.operation_name=نام عملیات
 dashboard.operation_switch=تعویض
@@ -2215,9 +2204,6 @@ repos.unadopted.no_more=هیچ مخزن تایید نشده دیگری یافت
 repos.owner=مالک
 repos.name=نام
 repos.private=خصوصی
-repos.watches=تماشا شده
-repos.stars=ستاره ها
-repos.forks=انشعاب‌ها
 repos.issues=مسائل
 repos.size=اندازه
 
@@ -2316,7 +2302,6 @@ auths.tip.nextcloud=با استفاده از منوی زیر "تنظیمات ->
 auths.tip.dropbox=یک برنامه جدید در https://www.dropbox.com/developers/apps بسازید
 auths.tip.facebook=`یک برنامه جدید در https://developers.facebook.com/apps بسازید برای ورود از طریق فیس بوک قسمت محصولات "Facebook Login"`
 auths.tip.github=یک برنامه OAuth جدید در https://github.com/settings/applications/new ثبت کنید
-auths.tip.gitlab=ثبت یک برنامه جدید در https://gitlab.com/profile/applications
 auths.tip.google_plus=اطلاعات مربوط به مشتری OAuth2 را از کلاینت API Google در https://console.developers.google.com/
 auths.tip.openid_connect=برای مشخص کردن نقاط پایانی از آدرس OpenID Connect Discovery URL (<server> /.well-known/openid-configuration) استفاده کنید.
 auths.tip.twitter=به https://dev.twitter.com/apps بروید ، برنامه ای ایجاد کنید و اطمینان حاصل کنید که گزینه "اجازه استفاده از این برنامه برای ورود به سیستم با Twitter" را فعال کنید
@@ -2501,6 +2486,7 @@ notices.desc=توضیحات
 notices.op=عملیات.
 notices.delete_success=گزارش سیستم حذف شده است.
 
+
 [action]
 create_repo=مخزن ایجاد شده <a href="%s"> %s</a>
 rename_repo=مخزن تغییر نام داد از <code>%[1]s</code> به <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini
index b6abb49a35..f283209908 100644
--- a/options/locale/locale_fi-FI.ini
+++ b/options/locale/locale_fi-FI.ini
@@ -114,6 +114,15 @@ concept_user_organization=Organisaatio
 
 name=Nimi
 
+filter=Suodata
+filter.is_archived=Arkistoidut
+filter.is_template=Malli
+filter.public=Julkinen
+filter.private=Yksityinen
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -243,7 +252,6 @@ collaborative_repos=Yhteistyö repot
 my_orgs=Organisaationi
 my_mirrors=Peilini
 view_home=Näytä %s
-search_repos=Etsi repo…
 filter=Muut suodattimet
 filter_by_team_repositories=Suodata tiimin repojen mukaan
 feed_of=`Syöte "%s"`
@@ -264,13 +272,7 @@ issues.in_your_repos=Repoissasi
 repos=Repot
 users=Käyttäjät
 organizations=Organisaatiot
-search=Hae
 code=Koodi
-search.match=Osuma
-repo_no_results=Vastaavia repoja ei löydy.
-user_no_results=Vastaavia käyttäjiä ei löytynyt.
-org_no_results=Ei löytynyt vastaavia organisaatioita.
-code_no_results=Hakuehtoasi vastaavaa lähdekoodia ei löytynyt.
 code_last_indexed_at=Viimeksi indeksoitu %s
 
 [auth]
@@ -283,7 +285,6 @@ remember_me=Muista tämä laite
 forgot_password_title=Unohtuiko salasana
 forgot_password=Unohtuiko salasana?
 sign_up_now=Tarvitsetko tilin? Rekisteröidy nyt.
-confirmation_mail_sent_prompt=Uusi varmistussähköposti on lähetetty osoitteeseen <b>%s</b>, ole hyvä ja tarkista saapuneet seuraavan %s tunnin sisällä saadaksesi rekisteröintiprosessin valmiiksi.
 must_change_password=Vaihda salasanasi
 allow_password_change=Vaadi käyttäjää vaihtamaan salasanansa (suositeltava)
 reset_password_mail_sent_prompt=Varmistussähköposti on lähetetty osoitteeseen <b>%s</b>. Tarkista saapuneet seuraavan %s tunnin sisällä saadaksesi tilin palauttamisen valmiiksi.
@@ -425,6 +426,7 @@ auth_failed=Todennus epäonnistui: %v
 
 target_branch_not_exist=Kohde branchia ei ole olemassa.
 
+
 [user]
 change_avatar=Vaihda profiilikuvasi…
 repositories=Repot
@@ -439,6 +441,7 @@ unfollow=Lopeta seuraaminen
 user_bio=Elämäkerta
 
 
+
 [settings]
 profile=Profiili
 account=Tili
@@ -554,7 +557,6 @@ gpg_key_verify=Vahvista
 gpg_token_required=Sinun täytyy antaa allekirjoitus alla olevalle pääsymerkille
 gpg_token=Pääsymerkki
 gpg_token_help=Voit luoda allekirjoituksen käyttäen:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Panssaroitu GPG-allekirjoitus
 key_signature_gpg_placeholder=Alkaa sanoilla '-----BEGIN PGP SIGNATURE-----'
 ssh_key_verified=Vahvistettu avain
@@ -656,7 +658,6 @@ visibility_helper_forced=Sivuston ylläpitäjä pakottaa uudet repot olemaan yks
 fork_repo=Forkkaa repo
 fork_from=Forkkaa lähteestä
 fork_visibility_helper=Forkatun repon näkyvyyttä ei voi muuttaa.
-clone_in_vsc=Kloonaa VS Codessa
 download_zip=Lataa ZIP
 download_tar=Lataa TAR.GZ
 repo_desc=Kuvaus
@@ -778,7 +779,6 @@ editor.require_signed_commit=Haara vaatii vahvistetun commitin
 
 commits.commits=Commitit
 commits.nothing_to_compare=Nämä haarat vastaavat toisiaan.
-commits.find=Haku
 commits.search_all=Kaikki haarat
 commits.author=Tekijä
 commits.message=Viesti
@@ -805,7 +805,6 @@ projects.edit=Muokkaa projektia
 projects.modify=Päivitä projekti
 projects.type.basic_kanban=Yksinkertainen Kanban
 projects.template.desc=Malli
-projects.type.uncategorized=Luokittelematon
 projects.column.edit_title=Nimi
 projects.column.new_title=Nimi
 projects.open=Avaa
@@ -982,7 +981,6 @@ pulls.has_viewed_file=Katsottu
 pulls.viewed_files_label=%[1]d / %[2]d tiedostoa katsottu
 pulls.compare_compare=vedä kohteesta
 pulls.filter_branch=Suodata branch
-pulls.no_results=Tuloksia ei löytynyt.
 pulls.nothing_to_compare=Nämä haarat vastaavat toisiaan. Ei ole tarvetta luoda vetopyyntöä.
 pulls.nothing_to_compare_and_allow_empty_pr=Nämä haarat vastaavat toisiaan. Vetopyyntö tulee olemaan tyhjä.
 pulls.has_pull_request=`Vetopyyntö haarojen välillä on jo olemassa: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1074,9 +1072,7 @@ activity.git_stats_and_deletions=ja
 activity.git_stats_deletion_1=%d poisto
 activity.git_stats_deletion_n=%d poistoa
 
-search=Haku
-search.match=Osuma
-search.code_no_results=Hakuehtoasi vastaavaa lähdekoodia ei löytynyt.
+contributors.contribution_type.commits=Commitit
 
 settings=Asetukset
 settings.options=Repo
@@ -1116,7 +1112,6 @@ settings.delete_desc=Repon poistaminen on pysyvä eikä voi peruuttaa.
 settings.delete_notices_1=- Tätä toimintoa <strong>EI VOI</strong> peruuttaa myöhemmin.
 settings.update_settings_success=Repon asetukset on päivitetty.
 settings.delete_collaborator=Poista
-settings.search_user_placeholder=Etsi käyttäjä…
 settings.teams=Tiimit
 settings.add_team=Lisää tiimi
 settings.add_webhook=Lisää webkoukku
@@ -1200,7 +1195,6 @@ settings.branch_protection=Haaran '<b>%s</b>' suojaus
 settings.protect_this_branch=Ota haaran suojaus käyttöön
 settings.protect_whitelist_deploy_keys=Lisää julkaisuavaimet sallittujen listalle mahdollistaaksesi repohin kirjoituksen.
 settings.protect_whitelist_users=Lista käyttäjistä joilla työntö oikeus:
-settings.protect_whitelist_search_users=Etsi käyttäjiä…
 settings.protect_merge_whitelist_committers_desc=Salli vain listaan merkittyjen käyttäjien ja tiimien yhdistää vetopyynnöt tähän haaraan.
 settings.protect_merge_whitelist_users=Lista käyttäjistä joilla yhdistämis-oikeus:
 settings.protect_required_approvals=Vaadittavat hyväksynnät:
@@ -1314,6 +1308,8 @@ topic.done=Valmis
 
 
 
+[graphs]
+
 [org]
 org_name_holder=Organisaatio
 org_full_name_holder=Organisaation täydellinen nimi
@@ -1402,6 +1398,8 @@ repositories=Repot
 authentication=Todennuslähteet
 emails=Käyttäjien sähköpostit
 config=Asetukset
+config_summary=Yhteenveto
+config_settings=Asetukset
 notices=Järjestelmän ilmoitukset
 monitor=Valvonta
 first_page=Ensimmäinen
@@ -1409,7 +1407,6 @@ last_page=Viimeisin
 total=Yhteensä: %d
 
 dashboard.statistic=Yhteenveto
-dashboard.operations=Huoltotoimet
 dashboard.system_status=Järjestelmän tila
 dashboard.operation_name=Toiminnon nimi
 dashboard.operation_switch=Vaihda
@@ -1503,9 +1500,6 @@ repos.repo_manage_panel=Repojen hallinta
 repos.owner=Omistaja
 repos.name=Nimi
 repos.private=Yksityinen
-repos.watches=Tarkkailijat
-repos.stars=Tähdet
-repos.forks=Haarat
 repos.issues=Ongelmat
 repos.size=Koko
 
@@ -1659,6 +1653,7 @@ notices.type_1=Repo
 notices.desc=Kuvaus
 notices.op=Toiminta
 
+
 [action]
 create_repo=luotu repo <a href="%s">%s</a>
 rename_repo=uudelleennimetty repo <code>%[1]s</code> nimelle <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index f3a264c1c8..dc66402901 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -123,6 +123,7 @@ pin=Épingler
 unpin=Désépingler
 
 artifacts=Artefacts
+confirm_delete_artifact=Êtes-vous sûr de vouloir supprimer l‘artefact « %s » ?
 
 archived=Archivé
 
@@ -141,6 +142,15 @@ confirm_delete_selected=Êtes-vous sûr de vouloir supprimer tous les éléments
 name=Nom
 value=Valeur
 
+filter=Filtrer
+filter.is_archived=Archivé
+filter.is_template=Modèle
+filter.public=Public
+filter.private=Privé
+
+
+[search]
+
 [aria]
 navbar=Barre de navigation
 footer=Pied de page
@@ -314,7 +324,6 @@ collaborative_repos=Dépôts collaboratifs
 my_orgs=Mes organisations
 my_mirrors=Mes miroirs
 view_home=Voir %s
-search_repos=Trouver un dépôt …
 filter=Autres filtres
 filter_by_team_repositories=Dépôts filtrés par équipe
 feed_of=Flux de « %s »
@@ -335,20 +344,8 @@ issues.in_your_repos=Dans vos dépôts
 repos=Dépôts
 users=Utilisateurs
 organizations=Organisations
-search=Rechercher
 go_to=Atteindre
 code=Code
-search.type.tooltip=Type de recherche
-search.fuzzy=Approximative
-search.fuzzy.tooltip=Inclure également les résultats proches de la recherche
-search.match=Exacte
-search.match.tooltip=Inclure uniquement les résultats exacts
-code_search_unavailable=Actuellement, la recherche de code n'est pas disponible. Veuillez contacter l'administrateur de votre site.
-repo_no_results=Aucun dépôt correspondant n'a été trouvé.
-user_no_results=Aucun utilisateur correspondant n'a été trouvé.
-org_no_results=Aucune organisation correspondante n'a été trouvée.
-code_no_results=Aucun code source correspondant à votre terme de recherche n'a été trouvé.
-code_search_results=Résultats de la recherche pour « %s »
 code_last_indexed_at=Dernière indexation %s
 relevant_repositories_tooltip=Les dépôts qui sont des forks ou qui n'ont aucun sujet, aucune icône et aucune description sont cachés.
 relevant_repositories=Seuls les dépôts pertinents sont affichés, <a href="%s">afficher les résultats non filtrés</a>.
@@ -361,11 +358,11 @@ disable_register_prompt=Les inscriptions sont désactivées. Veuillez contacter
 disable_register_mail=La confirmation par courriel à l’inscription est désactivée.
 manual_activation_only=Contactez l'administrateur de votre site pour terminer l'activation.
 remember_me=Mémoriser cet appareil
+remember_me.compromised=Le jeton de connexion n’est plus valide, ce qui peut indiquer un compte compromis. Veuillez inspecter les activités inhabituelles de votre compte.
 forgot_password_title=Mot de passe oublié
 forgot_password=Mot de passe oublié ?
 sign_up_now=Pas de compte ? Inscrivez-vous maintenant.
 sign_up_successful=Le compte a été créé avec succès. Bienvenue !
-confirmation_mail_sent_prompt=Un nouveau mail de confirmation a été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans les prochaines %s pour valider votre enregistrement.
 must_change_password=Réinitialisez votre mot de passe
 allow_password_change=Demande à l'utilisateur de changer son mot de passe (recommandé)
 reset_password_mail_sent_prompt=Un mail de confirmation a été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans les prochaines %s pour terminer la procédure de récupération du compte.
@@ -422,6 +419,7 @@ authorization_failed_desc=L'autorisation a échoué car nous avons détecté une
 sspi_auth_failed=Échec de l'authentification SSPI
 password_pwned=Le mot de passe que vous avez choisi se trouve sur la liste <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">des mots de passe ayant fuité</a> sur internet. Veuillez réessayer avec un mot de passe différent et considérer remplacer ce mot de passe si vous l'utilisez ailleurs.
 password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned
+last_admin=Vous ne pouvez pas supprimer ce compte car au moins un administrateur est requis.
 
 [mail]
 view_it_on=Voir sur %s
@@ -587,6 +585,8 @@ org_still_own_packages=Cette organisation possède encore un ou plusieurs paquet
 
 target_branch_not_exist=La branche cible n'existe pas.
 
+admin_cannot_delete_self=Vous ne pouvez pas vous supprimer vous-même lorsque vous êtes admin. Veuillez d’abord supprimer vos privilèges d’administrateur.
+
 [user]
 change_avatar=Changer votre avatar…
 joined_on=Inscrit le %s
@@ -612,6 +612,7 @@ form.name_reserved=Le nom d’utilisateur "%s" est réservé.
 form.name_pattern_not_allowed=Le motif « %s » n’est pas autorisé dans un nom de d'utilisateur.
 form.name_chars_not_allowed=Le nom d'utilisateur "%s" contient des caractères non valides.
 
+
 [settings]
 profile=Profil
 account=Compte
@@ -756,7 +757,6 @@ gpg_invalid_token_signature=La clé GPG, la signature et le jeton fournis ne cor
 gpg_token_required=Vous devez fournir une signature pour le jeton ci-dessous
 gpg_token=Jeton
 gpg_token_help=Vous pouvez générer une signature en utilisant :
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Signature GPG renforcée
 key_signature_gpg_placeholder=Commence par '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=La clé GPG "%s" a été vérifiée.
@@ -793,7 +793,7 @@ valid_until_date=Valable jusqu'au %s
 valid_forever=Valide pour toujours
 last_used=Dernière utilisation le
 no_activity=Aucune activité récente
-can_read_info=Lue(s)
+can_read_info=Lecture
 can_write_info=Écriture
 key_state_desc=Cette clé a été utilisée au cours des 7 derniers jours
 token_state_desc=Ce jeton a été utilisé au cours des 7 derniers jours
@@ -826,7 +826,7 @@ permissions_public_only=Publique uniquement
 permissions_access_all=Tout (public, privé et limité)
 select_permissions=Sélectionner les autorisations
 permission_no_access=Aucun accès
-permission_read=Lue(s)
+permission_read=Lecture
 permission_write=Lecture et écriture
 access_token_desc=Les autorisations des jetons sélectionnées se limitent aux <a %s>routes API</a> correspondantes. Lisez la <a %s>documentation</a> pour plus d’informations.
 at_least_one_permission=Vous devez sélectionner au moins une permission pour créer un jeton.
@@ -950,7 +950,6 @@ fork_branch=Branche à cloner sur la bifurcation
 all_branches=Toutes les branches
 fork_no_valid_owners=Ce dépôt ne peut pas être bifurqué car il n’a pas de propriétaire valide.
 use_template=Utiliser ce modèle
-clone_in_vsc=Cloner dans VS Code
 download_zip=Télécharger le ZIP
 download_tar=Télécharger le TAR.GZ
 download_bundle=Télécharger le BUNDLE
@@ -966,6 +965,8 @@ issue_labels_helper=Sélectionner un jeu de label.
 license=Licence
 license_helper=Sélectionner une licence
 license_helper_desc=Une licence réglemente ce que les autres peuvent ou ne peuvent pas faire avec votre code. Vous ne savez pas laquelle est la bonne pour votre projet ? Comment <a target="_blank" rel="noopener noreferrer" href="%s">choisir une licence</a>.
+object_format=Format d'objet
+object_format_helper=Format d’objet pour ce dépôt. Ne peut être modifié plus tard. SHA1 est le plus compatible.
 readme=LISEZMOI
 readme_helper=Choisissez un modèle de fichier LISEZMOI.
 readme_helper_desc=Le README est l'endroit idéal pour décrire votre projet et accueillir des contributeurs.
@@ -983,6 +984,7 @@ mirror_prune=Purger
 mirror_prune_desc=Supprimer les références externes obsolètes
 mirror_interval=Intervalle de synchronisation (les unités de temps valides sont 'h', 'm' et 's'). 0 pour désactiver la synchronisation automatique. (Intervalle minimum : %s)
 mirror_interval_invalid=L'intervalle de synchronisation est invalide.
+mirror_sync=synchronisé
 mirror_sync_on_commit=Synchroniser quand les révisions sont soumis
 mirror_address=Cloner depuis une URL
 mirror_address_desc=Insérez tous les identifiants requis dans la section Autorisation.
@@ -1033,6 +1035,7 @@ desc.public=Publique
 desc.template=Modèle
 desc.internal=Interne
 desc.archived=Archivé
+desc.sha256=SHA256
 
 template.items=Élément du modèle
 template.git_content=Contenu Git (branche par défaut)
@@ -1183,6 +1186,8 @@ audio_not_supported_in_browser=Votre navigateur ne supporte pas la balise « au
 stored_lfs=Stocké avec Git LFS
 symbolic_link=Lien symbolique
 executable_file=Fichiers exécutables
+vendored=Externe
+generated=Générée
 commit_graph=Graphe des révisions
 commit_graph.select=Sélectionner les branches
 commit_graph.hide_pr_refs=Masquer les demandes d'ajout
@@ -1269,9 +1274,7 @@ commits.desc=Naviguer dans l'historique des modifications.
 commits.commits=Révisions
 commits.no_commits=Pas de révisions en commun. "%s" et "%s" ont des historiques entièrement différents.
 commits.nothing_to_compare=Ces branches sont égales.
-commits.search=Rechercher des révisions…
 commits.search.tooltip=Vous pouvez utiliser les mots-clés "author:", "committer:", "after:", ou "before:" pour filtrer votre recherche, ex.: "revert author:Alice before:2019-01-13".
-commits.find=Chercher
 commits.search_all=Toutes les branches
 commits.author=Auteur
 commits.message=Message
@@ -1322,7 +1325,6 @@ projects.type.basic_kanban=Kanban basique
 projects.type.bug_triage=Bug à trier
 projects.template.desc=Modèle de projet
 projects.template.desc_helper=Sélectionnez un modèle de projet pour débuter
-projects.type.uncategorized=Non catégorisé
 projects.column.edit=Modifier la colonne
 projects.column.edit_title=Nom
 projects.column.new_title=Nom
@@ -1330,10 +1332,7 @@ projects.column.new_submit=Créer une colonne
 projects.column.new=Nouvelle colonne
 projects.column.set_default=Définir par défaut
 projects.column.set_default_desc=Les tickets et demandes d’ajout non-catégorisés seront placés dans cette colonne.
-projects.column.unset_default=Défaire par défaut
-projects.column.unset_default_desc=Les tickets et demandes d'ajouts non-catégorisés seront placés dans une colonne idoine.
 projects.column.delete=Supprimer la colonne
-projects.column.deletion_desc=La suppression d'une colonne de projet déplace tous les tickets liés à 'Non catégorisé'. Continuer ?
 projects.column.color=Couleur
 projects.open=Ouvrir
 projects.close=Fermer
@@ -1445,7 +1444,6 @@ issues.filter_sort.moststars=Favoris (décroissant)
 issues.filter_sort.feweststars=Favoris (croissant)
 issues.filter_sort.mostforks=Bifurcations (décroissant)
 issues.filter_sort.fewestforks=Bifurcations (croissant)
-issues.keyword_search_unavailable=La recherche par mot clé n'est pas disponible. Veuillez contacter l'administrateur de votre instance Gitea.
 issues.action_open=Ouvrir
 issues.action_close=Fermer
 issues.action_label=Label
@@ -1697,7 +1695,6 @@ pulls.compare_compare=tirer les modifications depuis
 pulls.switch_comparison_type=Changer le type de comparaison
 pulls.switch_head_and_base=Passez de head à base
 pulls.filter_branch=Filtre de branche
-pulls.no_results=Aucun résultat trouvé.
 pulls.show_all_commits=Afficher toutes les révisions
 pulls.show_changes_since_your_last_review=Affiche les modifications depuis votre dernière évaluation.
 pulls.showing_only_single_commit=Affiche uniquement les changements de la révision %[1]s
@@ -1706,6 +1703,7 @@ pulls.select_commit_hold_shift_for_range=Maintenir Maj et cliquer sur des révis
 pulls.review_only_possible_for_full_diff=Une évaluation n'est possible que lorsque vous affichez le différentiel complet.
 pulls.filter_changes_by_commit=Filtrer par révision
 pulls.nothing_to_compare=Ces branches sont identiques. Il n’y a pas besoin de créer une demande d'ajout.
+pulls.nothing_to_compare_have_tag=Les branches/étiquettes sélectionnées sont équivalentes.
 pulls.nothing_to_compare_and_allow_empty_pr=Ces branches sont égales. Cette demande d'ajout sera vide.
 pulls.has_pull_request='Il existe déjà une demande d'ajout entre ces deux branches : <a href="%[1]s">%[2]s#%[3]d</a>'
 pulls.create=Créer une demande d'ajout
@@ -1764,6 +1762,7 @@ pulls.merge_pull_request=Créer une révision de fusion
 pulls.rebase_merge_pull_request=Rebaser puis avancer rapidement
 pulls.rebase_merge_commit_pull_request=Rebaser puis créer une révision de fusion
 pulls.squash_merge_pull_request=Créer une révision de concaténation
+pulls.fast_forward_only_merge_pull_request=Avance rapide uniquement
 pulls.merge_manually=Fusionner manuellement
 pulls.merge_commit_id=L'ID de la révision de fusion
 pulls.require_signed_wont_sign=La branche nécessite des révisions signées mais cette fusion ne sera pas signée
@@ -1900,6 +1899,7 @@ wiki.page_name_desc=Entrez un nom pour cette page Wiki. Certains noms spéciaux
 wiki.original_git_entry_tooltip=Voir le fichier Git original au lieu d'utiliser un lien convivial.
 
 activity=Activité
+activity.navbar.contributors=Contributeurs
 activity.period.filter_label=Période :
 activity.period.daily=1 jour
 activity.period.halfweekly=3 jours
@@ -1965,16 +1965,10 @@ activity.git_stats_and_deletions=et
 activity.git_stats_deletion_1=%d suppression
 activity.git_stats_deletion_n=%d suppressions
 
-search=Chercher
-search.search_repo=Rechercher dans le dépôt
-search.type.tooltip=Type de recherche
-search.fuzzy=Approximative
-search.fuzzy.tooltip=Inclure également les résultats proches de la recherche
-search.match=Exacte
-search.match.tooltip=Inclure uniquement les résultats exacts
-search.results=Résultats de la recherche « %s » dans <a href="%s"> %s</a>
-search.code_no_results=Aucun code source correspondant à votre terme de recherche n'a été trouvé.
-search.code_search_unavailable=Actuellement, la recherche de code n'est pas disponible. Veuillez contacter l'administrateur de votre site.
+contributors.contribution_type.filter_label=Type de contribution :
+contributors.contribution_type.commits=Révisions
+contributors.contribution_type.additions=Ajouts
+contributors.contribution_type.deletions=Suppressions
 
 settings=Paramètres
 settings.desc=Les paramètres sont l'endroit où gérer les options du dépôt
@@ -2002,6 +1996,7 @@ settings.mirror_settings.docs.doc_link_title=Comment mettre en miroir les dépô
 settings.mirror_settings.docs.doc_link_pull_section=la section « Pulling from a remote repository » de la documentation.
 settings.mirror_settings.docs.pulling_remote_title=Tirer depuis un dépôt distant
 settings.mirror_settings.mirrored_repository=Dépôt en miroir
+settings.mirror_settings.pushed_repository=Dépôt sortant
 settings.mirror_settings.direction=Direction
 settings.mirror_settings.direction.pull=Tirer
 settings.mirror_settings.direction.push=Soumission
@@ -2053,6 +2048,7 @@ settings.pulls.default_allow_edits_from_maintainers=Autoriser les modifications
 settings.releases_desc=Activer les publications du dépôt
 settings.packages_desc=Activer le registre des paquets du dépôt
 settings.projects_desc=Activer les projets de dépôt
+settings.projects_mode_all=Tous les projets
 settings.actions_desc=Activer les actions du dépôt
 settings.admin_settings=Paramètres administrateur
 settings.admin_enable_health_check=Activer les vérifications de santé du dépôt (git fsck)
@@ -2127,7 +2123,6 @@ settings.delete_collaborator=Supprimer
 settings.collaborator_deletion=Supprimer le collaborateur
 settings.collaborator_deletion_desc=La suppression d'un collaborateur révoque son accès à ce dépôt. Continuer ?
 settings.remove_collaborator_success=Le collaborateur a été retiré.
-settings.search_user_placeholder=Rechercher un utilisateur…
 settings.org_not_allowed_to_be_collaborator=Les organisations ne peuvent être ajoutées en tant que collaborateur.
 settings.change_team_access_not_allowed=La modification de l'accès de l'équipe au dépôt a été limitée au propriétaire de l'organisation
 settings.team_not_in_organization=L'équipe n'est pas dans la même organisation que le dépôt
@@ -2135,7 +2130,6 @@ settings.teams=Équipes
 settings.add_team=Ajouter une équipe
 settings.add_team_duplicate=L'équipe a déjà le dépôt
 settings.add_team_success=L'équipe a maintenant accès au dépôt.
-settings.search_team=Rechercher une équipe…
 settings.change_team_permission_tip=La permission de l'équipe est définie sur la page de configuration de l'équipe et ne peut pas être modifiée par dépôt
 settings.delete_team_tip=Cette équipe a accès à tous les dépôts et ne peut pas être supprimée
 settings.remove_team_success=L'accès de l'équipe au dépôt a été supprimé.
@@ -2288,9 +2282,7 @@ settings.protect_whitelist_committers=Liste blanche des soumissions
 settings.protect_whitelist_committers_desc=Seuls les utilisateurs ou les équipes autorisés pourront soumettre sur cette branche (sans forcer).
 settings.protect_whitelist_deploy_keys=Mettez les clés de déploiement sur liste blanche avec accès en écriture pour soumettre.
 settings.protect_whitelist_users=Utilisateurs sur liste blanche :
-settings.protect_whitelist_search_users=Rechercher des utilisateurs…
 settings.protect_whitelist_teams=Équipes sur liste blanche :
-settings.protect_whitelist_search_teams=Rechercher des équipes…
 settings.protect_merge_whitelist_committers=Activer la liste blanche pour la fusion
 settings.protect_merge_whitelist_committers_desc=N'autoriser que les utilisateurs et les équipes en liste blanche d'appliquer les demandes de fusion sur cette branche.
 settings.protect_merge_whitelist_users=Utilisateurs en liste blanche de fusion :
@@ -2311,6 +2303,8 @@ settings.protect_approvals_whitelist_users=Évaluateurs autorisés :
 settings.protect_approvals_whitelist_teams=Équipes d’évaluateurs autorisés :
 settings.dismiss_stale_approvals=Révoquer automatiquement les approbations périmées
 settings.dismiss_stale_approvals_desc=Lorsque des nouvelles révisions changent le contenu de la demande d’ajout, les approbations existantes sont révoquées.
+settings.ignore_stale_approvals=Ignorer les approbations obsolètes
+settings.ignore_stale_approvals_desc=Ignorer les approbations d’anciennes révisions (évaluations obsolètes) du décompte des approbations de la demande d’ajout. Non pertinent quand les évaluations obsolètes sont déjà révoquées.
 settings.require_signed_commits=Exiger des révisions signées
 settings.require_signed_commits_desc=Rejeter les soumissions sur cette branche lorsqu'ils ne sont pas signés ou vérifiables.
 settings.protect_branch_name_pattern=Motif de nom de branche protégé
@@ -2366,6 +2360,7 @@ settings.archive.error=Une erreur s'est produite lors de l'archivage du dépôt.
 settings.archive.error_ismirror=Vous ne pouvez pas archiver un dépôt en miroir.
 settings.archive.branchsettings_unavailable=Le paramétrage des branches n'est pas disponible quand le dépôt est archivé.
 settings.archive.tagsettings_unavailable=Le paramétrage des étiquettes n'est pas disponible si le dépôt est archivé.
+settings.archive.mirrors_unavailable=Les miroirs ne sont pas disponibles lorsque le dépôt est archivé.
 settings.unarchive.button=Réhabiliter
 settings.unarchive.header=Réhabiliter ce dépôt
 settings.unarchive.text=Réhabiliter un dépôt dégèle les actions de révisions et de soumissions, la gestion des tickets et des demandes d'ajouts.
@@ -2532,7 +2527,6 @@ branch.default_deletion_failed=La branche "%s" est la branche par défaut. Elle
 branch.restore=`Restaurer la branche "%s"`
 branch.download=`Télécharger la branche "%s"`
 branch.rename=`Renommer la branche "%s"`
-branch.search=Rechercher une branche
 branch.included_desc=Cette branche fait partie de la branche par défaut
 branch.included=Incluses
 branch.create_new_branch=Créer une branche à partir de la branche :
@@ -2564,6 +2558,13 @@ error.csv.too_large=Impossible de visualiser le fichier car il est trop volumine
 error.csv.unexpected=Impossible de visualiser ce fichier car il contient un caractère inattendu ligne %d, colonne %d.
 error.csv.invalid_field_count=Impossible de visualiser ce fichier car il contient un nombre de champs incorrect à la ligne %d.
 
+[graphs]
+component_loading=Chargement de %s…
+component_loading_failed=Impossible de charger %s.
+component_loading_info=Ça prend son temps…
+component_failed_to_load=Une erreur inattendue s’est produite.
+contributors.what=contributions
+
 [org]
 org_name_holder=Nom de l'organisation
 org_full_name_holder=Nom complet de l'organisation
@@ -2668,7 +2669,6 @@ teams.write_permission_desc=Cette équipe permet l'accès en <strong>écriture</
 teams.admin_permission_desc=Cette équipe permet l'accès <strong>administrateur</strong> : les membres peuvent voir, participer et ajouter des collaborateurs à ses dépôts.
 teams.create_repo_permission_desc=De plus, cette équipe accorde la permission <strong>Créer un dépôt</strong> : les membres peuvent créer de nouveaux dépôts dans l'organisation.
 teams.repositories=Dépôts de l'Équipe
-teams.search_repo_placeholder=Rechercher dans le dépôt…
 teams.remove_all_repos_title=Supprimer tous les dépôts de l'équipe
 teams.remove_all_repos_desc=Ceci supprimera tous les dépôts de l'équipe.
 teams.add_all_repos_title=Ajouter tous les dépôts
@@ -2690,6 +2690,7 @@ teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindr
 
 [admin]
 dashboard=Tableau de bord
+self_check=Autodiagnostique
 identity_access=Identité et accès
 users=Comptes utilisateurs
 organizations=Organisations
@@ -2700,6 +2701,8 @@ integrations=Intégrations
 authentication=Sources d'authentification
 emails=Emails de l'utilisateur
 config=Configuration
+config_summary=Résumé
+config_settings=Paramètres
 notices=Informations
 monitor=Surveillance
 first_page=Première
@@ -2709,7 +2712,6 @@ settings=Paramètres administrateur
 
 dashboard.new_version_hint=Gitea %s est maintenant disponible, vous utilisez %s. Consultez <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">le blog</a> pour plus de détails.
 dashboard.statistic=Résumé
-dashboard.operations=Opérations de maintenance
 dashboard.system_status=État du système
 dashboard.operation_name=Nom de l'Opération
 dashboard.operation_switch=Basculer
@@ -2735,6 +2737,7 @@ dashboard.delete_missing_repos=Supprimer tous les dépôts dont les fichiers Git
 dashboard.delete_missing_repos.started=Tâche de suppression de tous les dépôts sans fichiers Git démarrée.
 dashboard.delete_generated_repository_avatars=Supprimer les avatars de dépôt générés
 dashboard.sync_repo_branches=Synchroniser les branches manquantes depuis Git vers la base de donnée.
+dashboard.sync_repo_tags=Synchroniser les étiquettes git depuis les dépôts vers la base de données
 dashboard.update_mirrors=Actualiser les miroirs
 dashboard.repo_health_check=Vérifier l'état de santé de tous les dépôts
 dashboard.check_repo_stats=Voir les statistiques de tous les dépôts
@@ -2789,6 +2792,7 @@ dashboard.stop_endless_tasks=Arrêter les tâches sans fin
 dashboard.cancel_abandoned_jobs=Annuler les jobs abandonnés
 dashboard.start_schedule_tasks=Démarrer les tâches planifiées
 dashboard.sync_branch.started=Début de la synchronisation des branches
+dashboard.sync_tag.started=Synchronisation des étiquettes
 dashboard.rebuild_issue_indexer=Reconstruire l’indexeur des tickets
 
 users.user_manage_panel=Gestion du compte utilisateur
@@ -2874,9 +2878,6 @@ repos.unadopted.no_more=Aucun dépôt dépossédé trouvé.
 repos.owner=Propriétaire
 repos.name=Nom
 repos.private=Privé
-repos.watches=Suivi par
-repos.stars=Votes
-repos.forks=Bifurcations
 repos.issues=Tickets
 repos.size=Taille
 repos.lfs_size=Taille LFS
@@ -3001,7 +3002,6 @@ auths.tip.nextcloud=`Enregistrez un nouveau consommateur OAuth sur votre instanc
 auths.tip.dropbox=Créez une nouvelle application sur https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Enregistrez une nouvelle application sur https://developers.facebook.com/apps et ajoutez le produit "Facebook Login"`
 auths.tip.github=Créez une nouvelle application OAuth sur https://github.com/settings/applications/new
-auths.tip.gitlab=Créez une nouvelle application sur https://gitlab.com/profile/applications
 auths.tip.google_plus=Obtenez des identifiants OAuth2 sur la console API de Google (https://console.developers.google.com/)
 auths.tip.openid_connect=Utilisez l'URL de découvert OpenID (<server>/.well-known/openid-configuration) pour spécifier les points d'accès
 auths.tip.twitter=Rendez-vous sur https://dev.twitter.com/apps, créez une application et assurez-vous que l'option "Autoriser l'application à être utilisée avec Twitter Connect" est activée
@@ -3215,6 +3215,13 @@ notices.desc=Description
 notices.op=Opération
 notices.delete_success=Les informations systèmes ont été supprimées.
 
+self_check.no_problem_found=Aucun problème trouvé pour l’instant.
+self_check.database_collation_mismatch=Exige que la base de données utilise la collation %s.
+self_check.database_collation_case_insensitive=La base de données utilise la collation %s, insensible à la casse. Bien que Gitea soit compatible, il peut y avoir quelques rares cas qui ne fonctionnent pas comme prévu.
+self_check.database_inconsistent_collation_columns=La base de données utilise la collation %s, mais ces colonnes utilisent des collations différentes. Cela peut causer des problèmes imprévus.
+self_check.database_fix_mysql=Pour les utilisateurs de MySQL ou MariaDB, vous pouvez utiliser la commande « gitea doctor convert » dans un terminal ou exécuter une requête du type « ALTER … COLLATE ... » pour résoudre les problèmes de collation.
+self_check.database_fix_mssql=Pour les utilisateurs de MSSQL, vous ne pouvez résoudre le problème qu’en exécutant une requête SQL du type « ALTER … COLLATE … ».
+
 [action]
 create_repo=a créé le dépôt <a href="%s">%s</a>
 rename_repo=a rebaptisé le dépôt <s><code>%[1]s</code></s> en <a href="%[2]s">%[3]s</a>
@@ -3399,6 +3406,9 @@ rpm.registry=Configurez ce registre à partir d'un terminal :
 rpm.distros.redhat=sur les distributions basées sur RedHat
 rpm.distros.suse=sur les distributions basées sur SUSE
 rpm.install=Pour installer le paquet, exécutez la commande suivante :
+rpm.repository=Informations sur le Dépôt
+rpm.repository.architectures=Architectures
+rpm.repository.multiple_groups=Ce paquet est disponible en plusieurs groupes.
 rubygems.install=Pour installer le paquet en utilisant gem, exécutez la commande suivante :
 rubygems.install2=ou ajoutez-le au Gemfile :
 rubygems.dependencies.runtime=Dépendances d'exécution
@@ -3531,8 +3541,8 @@ runs.actors_no_select=Tous les acteurs
 runs.status_no_select=Touts les statuts
 runs.no_results=Aucun résultat correspondant.
 runs.no_workflows=Il n'y a pas encore de workflows.
-runs.no_workflows.quick_start=Vous ne savez pas comment commencer avec Gitea Action ? Consultez <a target="_blank" rel="noopener noreferrer" href="%s">le guide de démarrage rapide</a>.
-runs.no_workflows.documentation=Pour plus d’informations sur les Actions Gitea, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>.
+runs.no_workflows.quick_start=Vous découvrez les Actions Gitea ? Consultez <a target="_blank" rel="noopener noreferrer" href="%s">le didacticiel</a>.
+runs.no_workflows.documentation=Pour plus d’informations sur les actions Gitea, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>.
 runs.no_runs=Le flux de travail n'a pas encore d'exécution.
 runs.empty_commit_message=(message de révision vide)
 
@@ -3551,7 +3561,7 @@ variables.none=Il n'y a pas encore de variables.
 variables.deletion=Retirer la variable
 variables.deletion.description=La suppression d’une variable est permanente et ne peut être défaite. Continuer ?
 variables.description=Les variables sont passées aux actions et ne peuvent être lues autrement.
-variables.id_not_exist=La variable numéro %d n’existe pas.
+variables.id_not_exist=La variable avec l’ID %d n’existe pas.
 variables.edit=Modifier la variable
 variables.deletion.failed=Impossible de retirer la variable.
 variables.deletion.success=La variable a bien été retirée.
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index aee4b44edf..fb229090d4 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -90,6 +90,14 @@ concept_user_organization=Szervezet
 
 name=Név
 
+filter.is_archived=Archivált
+filter.is_template=Sablon
+filter.public=Nyilvános
+filter.private=Privát
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -207,7 +215,6 @@ collaborative_repos=Együttműködési tárolók
 my_orgs=Szervezeteim
 my_mirrors=Tükreim
 view_home=Nézet %s
-search_repos=Tároló keresés…
 
 show_archived=Archivált
 
@@ -222,12 +229,7 @@ issues.in_your_repos=A tárolóidban
 repos=Tárolók
 users=Felhasználók
 organizations=Szervezetek
-search=Keresés
 code=Kód
-repo_no_results=Nincs ilyen tároló.
-user_no_results=Nincs ilyen felhasználó.
-org_no_results=Nincs ilyen szervezet.
-code_no_results=Nincs találat a keresési kifejezésedre.
 code_last_indexed_at=Utoljára indexelve: %s
 
 [auth]
@@ -240,7 +242,6 @@ remember_me=Eszköz megjegyzése
 forgot_password_title=Elfelejtett jelszó
 forgot_password=Elfelejtette a jelszavát?
 sign_up_now=Szeretne bejelentkezni? Regisztráljon most.
-confirmation_mail_sent_prompt=Új megerősítő email lett küldve ide: <b>%s</b>. Ellenőrizze postafiókját az elkövetkező %s a regisztrációs folyamat befejezéséhez.
 must_change_password=Jelszó módosítása
 allow_password_change=A felhasználóknak meg kell változtatniuk a jelszavukat(ajánlott)
 reset_password_mail_sent_prompt=Megerősítő email lett küldve ide: <b>%s</b>. Ellenőrizze postafiókját az elkövetkező %s a jelszó visszaállítási folyamat befejezéséhez.
@@ -369,6 +370,7 @@ auth_failed=A hitelesítés sikertelen: %v
 
 target_branch_not_exist=Cél ág nem létezik.
 
+
 [user]
 change_avatar=Profilkép megváltoztatása…
 repositories=Tárolók
@@ -383,6 +385,7 @@ unfollow=Követés törlése
 user_bio=Életrajz
 
 
+
 [settings]
 profile=Profil
 account=Fiók
@@ -721,8 +724,6 @@ editor.no_changes_to_show=Nincsen megjeleníthető változás.
 editor.add_subdir=Mappa hozzáadása…
 
 commits.commits=Commit-ok
-commits.search=Commit-ok keresése…
-commits.find=Keresés
 commits.search_all=Minden ág
 commits.author=Szerző
 commits.message=Üzenet
@@ -928,7 +929,6 @@ pulls.compare_changes=Új egyesítési kérés
 pulls.compare_base=egyesítés ide
 pulls.compare_compare=egyesítés innen
 pulls.filter_branch=Ágra szűrés
-pulls.no_results=Nincs találat.
 pulls.nothing_to_compare=Ezek az ágak egyenlőek. Nincs szükség egyesítési kérésre.
 pulls.create=Egyesítési kérés létrehozása
 pulls.title_desc=egyesíteni szeretné %[1]d változás(oka)t a(z) <code>%[2]s</code>-ból <code id="branch_target">%[3]s</code>-ba
@@ -1053,10 +1053,7 @@ activity.git_stats_and_deletions=és
 activity.git_stats_deletion_1=%d törlés
 activity.git_stats_deletion_n=%d törlés
 
-search=Keresés
-search.search_repo=Tároló keresés
-search.results=`"%s" találatok keresése itt: <a href="%s">%s</a>`
-search.code_no_results=Nincs találat a keresési kifejezésedre.
+contributors.contribution_type.commits=Commit-ok
 
 settings=Beállítások
 settings.options=Tároló
@@ -1099,8 +1096,6 @@ settings.branches=Ágak
 settings.protected_branch=Ág védeleme
 settings.protected_branch_can_push=Push engedélyezése?
 settings.protected_branch_can_push_yes=Most már push-olhatja
-settings.protect_whitelist_search_users=Felhasználó keresése…
-settings.protect_whitelist_search_teams=Csoportok keresése…
 settings.protect_check_status_contexts=Állapotellenőrzés engedélyezése
 settings.add_protected_branch=Védelem engedélyezése
 settings.delete_protected_branch=Védelem letiltása
@@ -1168,6 +1163,8 @@ topic.done=Kész
 
 
 
+[graphs]
+
 [org]
 org_name_holder=Szervezet neve
 org_full_name_holder=Szervezet teljes neve
@@ -1243,7 +1240,6 @@ teams.delete_team_desc=Egy csapat törlése visszavonja a tagjai hozzáférésé
 teams.delete_team_success=A csoport törölve lett.
 teams.read_permission_desc=Ez a csoport <strong>Olvasási</strong> jogosultságot biztosít: a tagok megtekinthetik és klónozhatják a csoport tárolóit.
 teams.repositories=Csoport tárolói
-teams.search_repo_placeholder=Tároló keresése…
 teams.remove_all_repos_title=Összes csapattároló eltávolítása
 teams.remove_all_repos_desc=Ez el fogja távolítani az összes tárolót a csoportból.
 teams.add_all_repos_title=Minden tároló hozzáadása
@@ -1261,6 +1257,8 @@ organizations=Szervezetek
 repositories=Tárolók
 authentication=Hitelesítési források
 config=Konfiguráció
+config_summary=Összefoglaló
+config_settings=Beállítások
 notices=Rendszer-értesítések
 monitor=Figyelés
 first_page=Első
@@ -1268,7 +1266,6 @@ last_page=Utolsó
 total=Összesen: %d
 
 dashboard.statistic=Összefoglaló
-dashboard.operations=Karbantartási műveletek
 dashboard.system_status=Rendszer Állapota
 dashboard.operation_name=Művelet Neve
 dashboard.operation_switch=Váltás
@@ -1347,8 +1344,6 @@ repos.repo_manage_panel=Tárolók Kezelése
 repos.owner=Tulajdonos
 repos.name=Név
 repos.private=Privát
-repos.watches=Figyelők
-repos.stars=Csillagok
 repos.issues=Hibajegyek
 repos.size=Méret
 
@@ -1410,7 +1405,6 @@ auths.tip.bitbucket=Igényeljen egy új OAuth jogosultságot itt: https://bitbuc
 auths.tip.dropbox=Vegyen fel új alkalmazást itt: https://www.dropbox.com/developers/apps
 auths.tip.facebook=Vegyen fel új alkalmazást itt: https://developers.facebook.com/apps majd adja hozzá a "Facebook Login"-t
 auths.tip.github=Vegyen fel új OAuth alkalmazást itt: https://github.com/settings/applications/new
-auths.tip.gitlab=Vegyen fel új alkalmazást itt: https://gitlab.com/profile/applications
 auths.tip.google_plus=Szerezzen OAuth2 kliens hitelesítési adatokat a Google API konzolban (https://console.developers.google.com/)
 auths.tip.openid_connect=Használja az OpenID kapcsolódás felfedező URL-t (<kiszolgáló>/.well-known/openid-configuration) a végpontok beállításához
 auths.tip.twitter=Menyjen ide: https://dev.twitter.com/apps, hozzon létre egy alkalmazást és győződjön meg róla, hogy az “Allow this application to be used to Sign in with Twitter” opció be van kapcsolva
@@ -1572,6 +1566,7 @@ notices.desc=Leírás
 notices.op=Op.
 notices.delete_success=A rendszer-értesítések törölve lettek.
 
+
 [action]
 create_repo=létrehozott tárolót: <a href="%s"> %s</a>
 rename_repo=átnevezte a(z) <code>%[1]s</code> tárolót <a href="%[2]s">%[3]s</a>-ra/re
diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini
index 4dd7c299df..96248cbc1d 100644
--- a/options/locale/locale_id-ID.ini
+++ b/options/locale/locale_id-ID.ini
@@ -83,6 +83,12 @@ concept_code_repository=Repositori
 
 name=Nama
 
+filter.is_template=Contoh
+filter.private=Pribadi
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -134,7 +140,6 @@ collaborative_repos=Repositori Kolaboratif
 my_orgs=Organisasi Saya
 my_mirrors=Duplikat Saya
 view_home=Lihat %s
-search_repos=Cari repositori…
 
 
 show_private=Pribadi
@@ -145,12 +150,7 @@ issues.in_your_repos=Dalam repositori anda
 repos=Repositori
 users=Pengguna
 organizations=Organisasi
-search=Cari
 code=Kode
-repo_no_results=Tidak ditemukan repositori yang cocok.
-user_no_results=Tidak ditemukan pengguna yang cocok.
-org_no_results=Tidak ada organisasi yang cocok ditemukan.
-code_no_results=Tidak ada kode sumber yang cocok dengan istilah yang anda cari.
 
 [auth]
 create_new_account=Daftar Akun
@@ -161,7 +161,6 @@ disable_register_mail=Konfirmasi lewat email untuk pengguna baru dimatikan.
 forgot_password_title=Lupa Kata Sandi
 forgot_password=Lupa kata sandi?
 sign_up_now=Butuh akun? Daftar sekarang.
-confirmation_mail_sent_prompt=Surel konfirmasi baru telah dikirim ke <b>%s</b>. Silakan periksa kotak masuk anda dalam %s ke depan untuk menyelesaikan proses pendaftaran.
 must_change_password=Perbarui kata sandi Anda
 allow_password_change=Wajibkan pengguna untuk mengganti kata sandi (disarankan)
 reset_password_mail_sent_prompt=Surel konfirmasi berhasil dikirim ke <b>%s</b>. Silahkan cek akun email Anda dalam %s jam untuk menyelesaikan proses pemulihan akun.
@@ -293,6 +292,7 @@ auth_failed=Otentikasi gagal: %v
 
 target_branch_not_exist=Target cabang tidak ada.
 
+
 [user]
 change_avatar=Ganti avatar anda…
 repositories=Repositori
@@ -306,6 +306,7 @@ unfollow=Berhenti Mengikuti
 user_bio=Biografi
 
 
+
 [settings]
 profile=Profil
 account=Akun
@@ -630,7 +631,6 @@ editor.cancel=Membatalkan
 editor.no_changes_to_show=Tidak ada perubahan untuk ditampilkan.
 
 commits.commits=Melakukan
-commits.find=Telusuri
 commits.author=Penulis
 commits.message=Pesan
 commits.date=Tanggal
@@ -748,7 +748,6 @@ issues.dependency.remove=Menghapus
 pulls.new=Permintaan Tarik Baru
 pulls.compare_changes=Permintaan Tarik Baru
 pulls.filter_branch=Penyaringan cabang
-pulls.no_results=Hasil tidak ditemukan.
 pulls.create=Buat Permintaan Tarik
 pulls.title_desc=ingin menggabungkan komit %[1]d dari <code>%[2]s</code> menuju <code id="branch_target">%[3]s</code>
 pulls.merged_title_desc=commit %[1]d telah digabungkan dari <code>%[2]s</code> menjadi <code>%[3]s</code> %[4]s
@@ -838,10 +837,7 @@ activity.title.releases_n=%d Rilis
 activity.title.releases_published_by=%s dikeluarkan oleh %s
 activity.published_release_label=Dikeluarkan
 
-search=Cari
-search.search_repo=Cari repositori
-search.results=Cari hasil untuk "%s" dalam <a href="%s">%s</a>
-search.code_no_results=Tidak ada kode sumber yang cocok dengan istilah yang anda cari.
+contributors.contribution_type.commits=Melakukan
 
 settings=Pengaturan
 settings.desc=Pengaturan dimana anda dapat mengelola pengaturan untuk repositori
@@ -870,7 +866,6 @@ settings.transfer_owner=Pemilik Baru
 settings.delete=Menghapus Repositori Ini
 settings.delete_notices_1=- Operasi ini <strong>TIDAK BISA</strong> dibatalkan.
 settings.delete_collaborator=Menghapus
-settings.search_user_placeholder=Cari pengguna…
 settings.teams=Tim
 settings.add_webhook=Tambahkan Webhook
 settings.webhook.test_delivery=Percobaan Pengiriman
@@ -953,6 +948,8 @@ branch.deleted_by=Dihapus oleh %s
 
 
 
+[graphs]
+
 [org]
 org_name_holder=Nama Organisasi
 org_full_name_holder=Organisasi Nama Lengkap
@@ -1003,13 +1000,13 @@ teams.update_settings=Memperbarui pengaturan
 teams.add_team_member=Tambahkan Anggota Tim
 teams.delete_team_success=Tim sudah di hapus.
 teams.repositories=Tim repositori
-teams.search_repo_placeholder=Cari repositori…
 
 [admin]
 dashboard=Dasbor
 organizations=Organisasi
 repositories=Repositori
 config=Konfigurasi
+config_settings=Pengaturan
 notices=Pemberitahuan Sistem
 monitor=Memantau
 first_page=Pertama
@@ -1072,8 +1069,6 @@ repos.repo_manage_panel=Manajemen Repositori
 repos.owner=Pemilik
 repos.name=Nama
 repos.private=Pribadi
-repos.watches=Jam tangan
-repos.stars=Bintang
 repos.issues=Masalah
 repos.size=Ukuran
 
@@ -1123,7 +1118,6 @@ auths.tip.oauth2_provider=Penyediaan OAuth2
 auths.tip.dropbox=Membuat aplikasi baru di https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Daftarkan sebuah aplikasi baru di https://developers.facebook.com/apps dan tambakan produk "Facebook Masuk"`
 auths.tip.github=Mendaftar aplikasi OAuth baru di https://github.com/settings/applications/new
-auths.tip.gitlab=Mendaftar aplikasi baru di https://gitlab.com/profile/applications
 auths.tip.openid_connect=Gunakan membuka ID yang terhubung ke jelajah URL (<server>/.well-known/openid-configuration) untuk menentukan titik akhir
 auths.delete=Menghapus Otentikasi Sumber
 auths.delete_auth_title=Menghapus Otentikasi Sumber
@@ -1262,6 +1256,7 @@ notices.desc=Deskripsi
 notices.op=Op.
 notices.delete_success=Laporan sistem telah dihapus.
 
+
 [action]
 create_repo=repositori dibuat <a href="%s">%s</a>
 rename_repo=ganti nama gudang penyimpanan dari <code>%[1]s</code> ke <a href="%[2]s">%[3]s</a>
@@ -1336,12 +1331,53 @@ runners.task_list.repository=Repositori
 runners.task_list.commit=Memperbuat
 
 runs.commit=Memperbuat
+runs.no_matching_online_runner_helper=Tidak ada runner online yang cocok dengan label: %s
+runs.actor=Aktor
+runs.status=Status
+runs.actors_no_select=Semua aktor
+runs.status_no_select=Semua status
+runs.no_results=Tidak ada hasil yang cocok.
+runs.no_workflows=Belum ada alur kerja.
+runs.no_workflows.quick_start=Tidak tahu cara memulai dengan Gitea Actions? Lihat <a target="_blank" rel="noopener noreferrer" href="%s">panduan cepat</a>.
+runs.no_workflows.documentation=Untuk informasi lebih lanjut tentang Gitea Actions, lihat <a target="_blank" rel="noopener noreferrer" href="%s">dokumentasi</a>.
+runs.no_runs=Alur kerja belum berjalan.
+runs.empty_commit_message=(pesan commit kosong)
 
+workflow.disable=Nonaktifkan Alur Kerja
+workflow.disable_success=Alur kerja '%s' berhasil dinonaktifkan.
+workflow.enable=Aktifkan Alur Kerja
+workflow.enable_success=Alur kerja '%s' berhasil diaktifkan.
+workflow.disabled=Alur kerja dinonaktifkan.
 
+need_approval_desc=Butuh persetujuan untuk menjalankan alur kerja untuk pull request fork.
 
+variables=Variabel
+variables.management=Managemen Variabel
+variables.creation=Tambah Variabel
+variables.none=Belum ada variabel.
+variables.deletion=Hapus variabel
+variables.deletion.description=Menghapus variabel bersifat permanen dan tidak dapat dibatalkan. Lanjutkan?
+variables.description=Variabel akan diteruskan ke beberapa tindakan dan tidak dapat dibaca sebaliknya.
+variables.id_not_exist=Variabel dengan ID %d tidak ada.
+variables.edit=Edit Variabel
+variables.deletion.failed=Gagal menghapus variabel.
+variables.deletion.success=Variabel telah dihapus.
+variables.creation.failed=Gagal menambahkan variabel.
+variables.creation.success=Variabel "%s" telah ditambahkan.
+variables.update.failed=Gagal mengedit variabel.
+variables.update.success=Variabel telah diedit.
 
 [projects]
+type-1.display_name=Proyek Individu
+type-2.display_name=Proyek Repositori
+type-3.display_name=Proyek Organisasi
 
 [git.filemode]
+changed_filemode=%[1]s → %[2]s
 ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Directory
+normal_file=Normal file
+executable_file=Executable file
+symbolic_link=Symbolic link
+submodule=Submodule
 
diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini
index 2ba623dc12..3165c4185b 100644
--- a/options/locale/locale_is-IS.ini
+++ b/options/locale/locale_is-IS.ini
@@ -111,6 +111,14 @@ concept_code_repository=Hugbúnaðarsafn
 name=Heiti
 value=Gildi
 
+filter=Sía
+filter.is_archived=Safnvistað
+filter.is_template=Sniðmát
+filter.public=Opinbert
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -224,7 +232,6 @@ show_more_repos=Sýna fleiri hugbúnaðarsöfn…
 my_orgs=Stofnanir Mínar
 my_mirrors=Speglanir Mínar
 view_home=Skoða %s
-search_repos=Finna hugbúnaðarsafn…
 filter=Aðrar Síur
 
 show_archived=Safnvistað
@@ -239,14 +246,7 @@ issues.in_your_repos=Í hugbúnaðarsöfnum þínum
 repos=Hugbúnaðarsöfn
 users=Notendur
 organizations=Stofnanir
-search=Leita
 code=Kóði
-search.fuzzy=Óljóst
-code_search_unavailable=Sem stendur er kóðaleit ekki í boði. Vinsamlegast hafðu samband við síðustjórann þinn.
-repo_no_results=Engin samsvarandi hugbúnaðarsöfn fundust.
-user_no_results=Engir samsvarandi notendur fundust.
-org_no_results=Engar samsvarandi stofnanir fundust.
-code_no_results=Enginn samsvarandi frumkóði fannst eftur þínum leitarorðum.
 
 [auth]
 create_new_account=Skrá Notanda
@@ -401,6 +401,7 @@ team_not_exist=Liðið er ekki til.
 
 
 
+
 [user]
 change_avatar=Breyttu notandamyndinni þinni…
 repositories=Hugbúnaðarsöfn
@@ -417,6 +418,7 @@ user_bio=Lífssaga
 disabled_public_activity=Þessi notandi hefur slökkt á opinberum sýnileika virkninnar.
 
 
+
 [settings]
 profile=Notandasíða
 account=Reikningur
@@ -703,7 +705,6 @@ editor.cancel=Hætta við
 editor.fail_to_update_file_summary=Villuskilaboð:
 
 commits.commits=Framlög
-commits.find=Leita
 commits.author=Höfundur
 commits.message=Skilaboð
 commits.date=Dagsetning
@@ -727,7 +728,6 @@ projects.edit=Breyta Verkefnum
 projects.modify=Uppfæra Verkefni
 projects.type.none=Ekkert
 projects.template.desc=Sniðmát
-projects.type.uncategorized=Óflokkuð
 projects.column.edit_title=Heiti
 projects.column.new_title=Heiti
 projects.column.color=Litað
@@ -989,10 +989,7 @@ activity.git_stats_and_deletions=og
 activity.git_stats_deletion_1=%d eyðing
 activity.git_stats_deletion_n=%d eyðingar
 
-search=Leita
-search.fuzzy=Óljóst
-search.code_no_results=Enginn samsvarandi frumkóði fannst eftur þínum leitarorðum.
-search.code_search_unavailable=Sem stendur er kóðaleit ekki í boði. Vinsamlegast hafðu samband við síðustjórann þinn.
+contributors.contribution_type.commits=Framlög
 
 settings=Stillingar
 settings.options=Hugbúnaðarsafn
@@ -1112,6 +1109,8 @@ topic.done=Í lagi
 
 
 
+[graphs]
+
 [org]
 repo_updated=Uppfært
 members=Meðlimar
@@ -1158,6 +1157,8 @@ teams.all_repositories=Öll hugbúnaðarsöfn
 [admin]
 repositories=Hugbúnaðarsöfn
 config=Stilling
+config_summary=Yfirlit
+config_settings=Stillingar
 first_page=Byrjun
 last_page=Síðasta
 total=Samtals: %d
@@ -1196,9 +1197,6 @@ orgs.members=Meðlimar
 
 repos.owner=Eigandi
 repos.name=Heiti
-repos.watches=Fylgist með
-repos.stars=Eftirlæti
-repos.forks=Skiptingar
 repos.issues=Vandamál
 repos.size=Stærð
 
@@ -1278,6 +1276,7 @@ notices.type_1=Hugbúnaðarsafn
 notices.type_2=Verkefni
 notices.desc=Lýsing
 
+
 [action]
 create_issue=`opnaði vandamál <a href="%[1]s">%[3]s#%[2]s</a>`
 reopen_issue=`enduropnaði vandamál <a href="%[1]s">%[3]s#%[2]s</a>`
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index a30232dd10..9a22995dfb 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -116,6 +116,15 @@ concept_user_organization=Organizzazione
 name=Nome
 value=Valore
 
+filter=Filtro
+filter.is_archived=Archiviato
+filter.is_template=Template
+filter.public=Pubblico
+filter.private=Privati
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -254,7 +263,6 @@ collaborative_repos=Repository Condivisi
 my_orgs=Le mie Organizzazioni
 my_mirrors=I miei Mirror
 view_home=Vedi %s
-search_repos=Trova un repository…
 filter=Altro filtri
 filter_by_team_repositories=Filtra per repository del team
 feed_of=`Feed di "%s"`
@@ -275,15 +283,7 @@ issues.in_your_repos=Nei tuoi repository
 repos=Repository
 users=Utenti
 organizations=Organizzazioni
-search=Cerca
 code=Codice
-search.fuzzy=Fuzzy
-search.match=Corrispondenze
-code_search_unavailable=Attualmente la ricerca di codice non è disponibile. Contatta l'amministratore del sito.
-repo_no_results=Nessuna repository corrispondente.
-user_no_results=Nessun utente corrispondente.
-org_no_results=Nessun'organizzazione corrispondente trovata.
-code_no_results=Nessun codice sorgente corrispondente ai termini di ricerca.
 code_last_indexed_at=Ultimo indicizzato %s
 
 [auth]
@@ -297,7 +297,6 @@ remember_me=Ricorda questo dispositivo
 forgot_password_title=Password Dimenticata
 forgot_password=Password dimenticata?
 sign_up_now=Hai bisogno di un account? Registrati adesso.
-confirmation_mail_sent_prompt=Una nuova email di conferma è stata inviata a <b>%s</b>. Per favore controlla la tua posta in arrivo nelle prossime %s per completare il processo di registrazione.
 must_change_password=Aggiorna la tua password
 allow_password_change=Richiede all'utente di cambiare la password (scelta consigliata)
 reset_password_mail_sent_prompt=Una email di conferma è stata inviata a <b>%s</b>. Per favore controlla la tua posta in arrivo nelle prossime %s per completare il processo di reset della password.
@@ -490,6 +489,7 @@ auth_failed=Autenticazione non riuscita: %v
 
 target_branch_not_exist=Il ramo (branch) di destinazione non esiste.
 
+
 [user]
 change_avatar=Modifica il tuo avatar…
 repositories=Repository
@@ -506,6 +506,7 @@ user_bio=Biografia
 disabled_public_activity=L'utente ha disabilitato la vista pubblica dell'attività.
 
 
+
 [settings]
 profile=Profilo
 account=Account
@@ -633,7 +634,6 @@ gpg_invalid_token_signature=La chiave GPG fornita, la firma e il token non corri
 gpg_token_required=Devi fornire una firma per il token sottostante
 gpg_token=Token
 gpg_token_help=È possibile generare una firma utilizzando:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Firma GPG corazzata
 key_signature_gpg_placeholder=Comincia con '-----BEGIN PGP SIGNATURE-----'
 ssh_key_verified=Chiave Verificata
@@ -786,7 +786,6 @@ already_forked=Hai già fatto il fork di %s
 fork_to_different_account=Fai Fork a un account diverso
 fork_visibility_helper=La visibilità di un repository forkato non può essere modificata.
 use_template=Usa questo modello
-clone_in_vsc=Clona nel codice VS
 download_zip=Scarica ZIP
 download_tar=Scarica TAR.GZ
 download_bundle=Scarica BUNDLE
@@ -1050,8 +1049,6 @@ editor.revert=Ripristina %s su:
 commits.desc=Sfoglia la cronologia di modifiche del codice rogente.
 commits.commits=Commit
 commits.nothing_to_compare=Questi rami sono uguali.
-commits.search=Ricerca commits…
-commits.find=Cerca
 commits.search_all=Tutti i branch
 commits.author=Autore
 commits.message=Messaggio
@@ -1096,7 +1093,6 @@ projects.type.basic_kanban=Basic Kanban
 projects.type.bug_triage=Bug Triage
 projects.template.desc=Template di progetto
 projects.template.desc_helper=Seleziona un modello di progetto per iniziare
-projects.type.uncategorized=Senza categoria
 projects.column.edit_title=Nome
 projects.column.new_title=Nome
 projects.column.color=Colore
@@ -1408,7 +1404,6 @@ pulls.compare_compare=esegui un pull da
 pulls.switch_comparison_type=Cambia tipo di confronto
 pulls.switch_head_and_base=Testa e base di commutazione
 pulls.filter_branch=Filtra branch
-pulls.no_results=Nessun risultato trovato.
 pulls.nothing_to_compare=Questi rami sono uguali. Non c'è alcuna necessità di creare una pull request.
 pulls.nothing_to_compare_and_allow_empty_pr=Questi rami sono uguali. Questa PR sarà vuota.
 pulls.has_pull_request=`Una pull request tra questi rami esiste già: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1623,13 +1618,7 @@ activity.git_stats_and_deletions=e
 activity.git_stats_deletion_1=%d cancellazione
 activity.git_stats_deletion_n=%d cancellazioni
 
-search=Ricerca
-search.search_repo=Ricerca repository
-search.fuzzy=Fuzzy
-search.match=Corrispondenze
-search.results=Risultati della ricerca per "%s" in <a href="%s">%s</a>
-search.code_no_results=Nessun codice sorgente corrispondente al termine di ricerca trovato.
-search.code_search_unavailable=Attualmente la ricerca di codice non è disponibile. Contatta l'amministratore del sito.
+contributors.contribution_type.commits=Commit
 
 settings=Impostazioni
 settings.desc=Impostazioni ti permette di gestire le impostazioni del repository
@@ -1757,7 +1746,6 @@ settings.delete_collaborator=Rimuovi
 settings.collaborator_deletion=Rimuovi collaboratore
 settings.collaborator_deletion_desc=Rimuovere un collaboratore revocherà l'accesso a questo repository. Continuare?
 settings.remove_collaborator_success=Il collaboratore è stato rimosso.
-settings.search_user_placeholder=Ricerca utente…
 settings.org_not_allowed_to_be_collaborator=Le organizzazioni non possono essere aggiunte come un collaboratore.
 settings.change_team_access_not_allowed=La modifica dell'accesso al team per il repository è stato limitato al solo proprietario dell'organizzazione
 settings.team_not_in_organization=Il team non è nella stessa organizzazione del repository
@@ -1765,7 +1753,6 @@ settings.teams=Gruppi
 settings.add_team=Aggiungi Squadra
 settings.add_team_duplicate=Il team ha già il repository
 settings.add_team_success=Il team ha ora accesso al repository.
-settings.search_team=Cerca Squadra…
 settings.change_team_permission_tip=Il permesso del team è impostato sulla pagina delle impostazioni del team e non può essere modificato per repository
 settings.delete_team_tip=Questo team ha accesso a tutte le repository e non può essere rimosso
 settings.remove_team_success=L'accesso del team al repository è stato rimosso.
@@ -1904,9 +1891,7 @@ settings.protect_whitelist_committers=Lista bianch push ristretti
 settings.protect_whitelist_committers_desc=Solo gli utenti o i team nella whitelist potranno pushare su questo ramo (ma non forzare il push).
 settings.protect_whitelist_deploy_keys=Chiavi di deploy in whitelist con permessi di scrittura per il push.
 settings.protect_whitelist_users=Utenti nella whitelist per pushare:
-settings.protect_whitelist_search_users=Cerca utenti…
 settings.protect_whitelist_teams=Team nella whitelist per pushare:
-settings.protect_whitelist_search_teams=Ricerca team…
 settings.protect_merge_whitelist_committers=Attiva la whitelist per i merge
 settings.protect_merge_whitelist_committers_desc=Consentire soltanto agli utenti o ai team in whitelist il permesso di unire le pull request di questo branch.
 settings.protect_merge_whitelist_users=Utenti nella whitelist per il merging:
@@ -2117,6 +2102,8 @@ error.csv.too_large=Impossibile visualizzare questo file perché è troppo grand
 error.csv.unexpected=Impossibile visualizzare questo file perché contiene un carattere inatteso alla riga %d e alla colonna %d.
 error.csv.invalid_field_count=Impossibile visualizzare questo file perché ha un numero errato di campi alla riga %d.
 
+[graphs]
+
 [org]
 org_name_holder=Nome dell'Organizzazione
 org_full_name_holder=Nome completo dell'organizzazione
@@ -2213,7 +2200,6 @@ teams.write_permission_desc=Questo team concede l'accesso di <strong>Scrittura</
 teams.admin_permission_desc=Questo team concede l'accesso di <strong>Amministratore</strong>: i membri possono leggere da, pushare su e aggiungere collaboratori ai repository del team.
 teams.create_repo_permission_desc=Inoltre, questo team concede il permesso di <strong>Creare repository</strong>: i membri possono creare nuove repository nell'organizzazione.
 teams.repositories=Repository di Squadra
-teams.search_repo_placeholder=Ricerca repository…
 teams.remove_all_repos_title=Rimuovi tutti i repository del team
 teams.remove_all_repos_desc=Questo rimuoverà tutte le repository dal team.
 teams.add_all_repos_title=Aggiungi tutti i repository
@@ -2238,6 +2224,8 @@ hooks=Webhooks
 authentication=Fonti di autenticazione
 emails=Email Utente
 config=Configurazione
+config_summary=Riepilogo
+config_settings=Impostazioni
 notices=Avvisi di sistema
 monitor=Monitoraggio
 first_page=Prima
@@ -2245,7 +2233,6 @@ last_page=Ultima
 total=Totale: %d
 
 dashboard.statistic=Riepilogo
-dashboard.operations=Operazioni di manutenzione
 dashboard.system_status=Stato del sistema
 dashboard.operation_name=Nome Operazione
 dashboard.operation_switch=Cambia
@@ -2392,9 +2379,6 @@ repos.unadopted.no_more=Nessun repository non adottato trovato
 repos.owner=Proprietario
 repos.name=Nome
 repos.private=Privati
-repos.watches=Segue
-repos.stars=Voti
-repos.forks=Fork
 repos.issues=Problemi
 repos.size=Dimensione
 
@@ -2510,7 +2494,6 @@ auths.tip.nextcloud=`Registra un nuovo OAuth sulla tua istanza utilizzando il se
 auths.tip.dropbox=Crea una nuova applicazione su https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Registra una nuova applicazione su https://developers.facebook.com/apps e aggiungi il prodotto "Facebook Login"`
 auths.tip.github=Registra una nuova applicazione OAuth su https://github.com/settings/applications/new
-auths.tip.gitlab=Registra una nuova applicazione su https://gitlab.com/profile/applications
 auths.tip.google_plus=Ottieni le credenziali del client OAuth2 dalla console API di Google su https://console.developers.google.com/
 auths.tip.openid_connect=Utilizza l'OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) per specificare gli endpoint
 auths.tip.twitter=Vai su https://dev.twitter.com/apps, crea una applicazione e assicurati che l'opzione "Allow this application to be used to Sign In with Twitter" sia abilitata
@@ -2703,6 +2686,7 @@ notices.desc=Descrizione
 notices.op=Op.
 notices.delete_success=Gli avvisi di sistema sono stati eliminati.
 
+
 [action]
 create_repo=ha creato il repository <a href="%s">%s</a>
 rename_repo=repository rinominato da <code>%[1]s</code> a <a href="%[2]s">[3]s</a>
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 9216277955..0edd6c5dd7 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -113,6 +113,7 @@ loading=読み込み中…
 error=エラー
 error404=アクセスしようとしたページは<strong>存在しない</strong>か、閲覧が<strong>許可されていません</strong>。
 go_back=戻る
+invalid_data=無効なデータ: %v
 
 never=無し
 unknown=不明
@@ -123,6 +124,7 @@ pin=ピン留め
 unpin=ピン留め解除
 
 artifacts=成果物
+confirm_delete_artifact=アーティファクト %s を削除してよろしいですか?
 
 archived=アーカイブ
 
@@ -141,6 +143,43 @@ confirm_delete_selected=選択したすべてのアイテムを削除してよ
 name=名称
 value=値
 
+filter=フィルター
+filter.clear=フィルターをクリア
+filter.is_archived=アーカイブ
+filter.not_archived=非アーカイブ
+filter.is_fork=フォーク
+filter.not_fork=非フォーク
+filter.is_mirror=ミラー
+filter.not_mirror=非ミラー
+filter.is_template=テンプレート
+filter.not_template=非テンプレート
+filter.public=公開
+filter.private=プライベート
+
+no_results_found=見つかりません。
+
+[search]
+search=検索…
+type_tooltip=検索タイプ
+fuzzy=あいまい
+fuzzy_tooltip=検索ワードに近い結果も含めます
+match=一致
+match_tooltip=検索ワードと完全に一致する結果のみ含めます
+repo_kind=リポジトリを検索...
+user_kind=ユーザーを検索...
+org_kind=組織を検索...
+team_kind=チームを検索…
+code_kind=コードを検索...
+code_search_unavailable=現在コード検索は利用できません。 サイト管理者にお問い合わせください。
+code_search_by_git_grep=現在のコード検索結果は "git grep" で提供されています。 サイト管理者がリポジトリインデクサーを有効にすると、より良い結果が得られるかもしれません。
+package_kind=パッケージを検索...
+project_kind=プロジェクトを検索...
+branch_kind=ブランチを検索...
+commit_kind=コミットを検索...
+runner_kind=ランナーを検索...
+no_results=一致する結果が見つかりませんでした
+keyword_search_unavailable=現在キーワード検索は利用できません。 サイト管理者にお問い合わせください。
+
 [aria]
 navbar=ナビゲーションバー
 footer=フッター
@@ -148,8 +187,8 @@ footer.software=ソフトウェアについて
 footer.links=リンク
 
 [heatmap]
-number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 個の貢献
-no_contributions=貢献なし
+number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 件の実績
+no_contributions=実績なし
 less=少
 more=多
 
@@ -246,6 +285,7 @@ email_title=メール設定
 smtp_addr=SMTPホスト
 smtp_port=SMTPポート
 smtp_from=メール送信者
+smtp_from_invalid=「メール送信者」のアドレスが無効です
 smtp_from_helper=Giteaが使用するメールアドレス。 メールアドレスのみ、または、 "名前" <email@example.com> の形式で入力してください。
 mailer_user=SMTPユーザー名
 mailer_password=SMTPパスワード
@@ -305,6 +345,7 @@ env_config_keys=環境設定
 env_config_keys_prompt=以下の環境変数も設定ファイルに適用されます:
 
 [home]
+nav_menu=ナビゲーションメニュー
 uname_holder=ユーザー名またはメールアドレス
 password_holder=パスワード
 switch_dashboard_context=ダッシュボードのコンテキスト切替
@@ -314,7 +355,6 @@ collaborative_repos=共同リポジトリ
 my_orgs=自分の組織
 my_mirrors=自分のミラー
 view_home=%s を表示
-search_repos=リポジトリを探す…
 filter=その他のフィルター
 filter_by_team_repositories=チームリポジトリで絞り込み
 feed_of=`"%s" のフィード`
@@ -335,20 +375,8 @@ issues.in_your_repos=あなたのリポジトリ
 repos=リポジトリ
 users=ユーザー
 organizations=組織
-search=検索
 go_to=開く
 code=コード
-search.type.tooltip=検索タイプ
-search.fuzzy=あいまい
-search.fuzzy.tooltip=検索ワードにおおよそ一致している結果も含めます
-search.match=一致
-search.match.tooltip=検索ワードに一致する結果だけを含めます
-code_search_unavailable=現在コード検索は利用できません。 サイト管理者にお問い合わせください。
-repo_no_results=一致するリポジトリが見つかりません。
-user_no_results=一致するユーザーが見つかりません。
-org_no_results=一致する組織が見つかりません。
-code_no_results=検索ワードに一致するソースコードが見つかりません。
-code_search_results=`"%s" の検索結果`
 code_last_indexed_at=最終取得 %s
 relevant_repositories_tooltip=フォークリポジトリや、トピック、アイコン、説明のいずれも無いリポジトリは表示されません。
 relevant_repositories=妥当と思われるリポジトリのみを表示しています。 <a href="%s">フィルタリングしない結果を表示</a>。
@@ -366,7 +394,6 @@ forgot_password_title=パスワードを忘れた
 forgot_password=パスワードをお忘れですか?
 sign_up_now=アカウントが必要ですか? 今すぐ登録しましょう。
 sign_up_successful=アカウントは無事に作成されました。ようこそ!
-confirmation_mail_sent_prompt=<b>%s</b> に確認メールを送信しました。 %s以内に受信トレイを確認し、登録手続きを完了してください。
 must_change_password=パスワードの更新
 allow_password_change=ユーザーはパスワードの変更が必要 (推奨)
 reset_password_mail_sent_prompt=<b>%s</b> に確認メールを送信しました。 %s以内に受信トレイを確認し、アカウント回復手続きを完了してください。
@@ -423,6 +450,7 @@ authorization_failed_desc=無効なリクエストを検出したため認可が
 sspi_auth_failed=SSPI認証に失敗しました
 password_pwned=あなたが選択したパスワードは、過去の情報漏洩事件で流出した<a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">盗まれたパスワードのリスト</a>に含まれています。 別のパスワードでもう一度試してください。 また他の登録でもこのパスワードからの変更を検討してください。
 password_pwned_err=HaveIBeenPwnedへのリクエストを完了できませんでした
+last_admin=最後の管理者は削除できません。少なくとも一人の管理者が必要です。
 
 [mail]
 view_it_on=%s で見る
@@ -588,6 +616,8 @@ org_still_own_packages=組織はまだ1つ以上のパッケージを所有し
 
 target_branch_not_exist=ターゲットのブランチが存在していません。
 
+admin_cannot_delete_self=あなたが管理者である場合、自分自身を削除することはできません。最初に管理者権限を削除してください。
+
 [user]
 change_avatar=アバターを変更…
 joined_on=%sに登録
@@ -613,6 +643,30 @@ form.name_reserved=ユーザー名 "%s" は予約されています。
 form.name_pattern_not_allowed=`"%s" の形式はユーザー名に使用できません。`
 form.name_chars_not_allowed=ユーザー名 "%s" には無効な文字が含まれています。
 
+block.block=ブロック
+block.block.user=ユーザーをブロック
+block.block.org=組織向けにユーザーをブロック
+block.block.failure=ユーザーのブロックに失敗しました: %s
+block.unblock=ブロックを解除
+block.unblock.failure=ユーザーのブロック解除に失敗しました: %s
+block.blocked=あなたはこのユーザーをブロックしています。
+block.title=ユーザーをブロックする
+block.info=ユーザーをブロックすると、そのユーザーは、プルリクエストやイシューの作成、コメントの投稿など、リポジトリに対する操作ができなくなります。 ユーザーのブロックについてはよく確認してください。
+block.info_1=ユーザーをブロックすることで、あなたのアカウントとあなたのリポジトリに対する以下の行為を阻止します:
+block.info_2=あなたのアカウントのフォロー
+block.info_3=あなたのユーザー名で@メンションして通知を送ること
+block.info_4=そのユーザーのリポジトリに、あなたを共同作業者として招待すること
+block.info_5=リポジトリへの、スター、フォーク、ウォッチ
+block.info_6=イシューやプルリクエストの作成、コメント投稿
+block.info_7=イシューやプルリクエストでの、あなたのコメントに対するリアクションの送信
+block.user_to_block=ブロックするユーザー
+block.note=メモ
+block.note.title=メモ(任意):
+block.note.info=メモはブロックされるユーザーには表示されません。
+block.note.edit=メモを編集
+block.list=ブロックしたユーザー
+block.list.none=ブロックしているユーザーはいません。
+
 [settings]
 profile=プロフィール
 account=アカウント
@@ -655,8 +709,8 @@ language=言語
 ui=テーマ
 hidden_comment_types=非表示にするコメントの種類
 hidden_comment_types_description=ここでチェックを入れたコメントの種類は、イシューのページには表示されません。 たとえば「ラベル」にチェックを入れると、「<ユーザー> が <ラベル> を追加/削除」といったコメントはすべて除去されます。
-hidden_comment_types.ref_tooltip=このイシューが別のイシューやコミット等から参照されたというコメント
-hidden_comment_types.issue_ref_tooltip=このイシューに関連付けるブランチやタグをユーザーが変更したというコメント
+hidden_comment_types.ref_tooltip=このイシューが別のイシューやコミット等から参照された、というコメント
+hidden_comment_types.issue_ref_tooltip=このイシューのブランチやタグへの関連付けをユーザーが変更した、というコメント
 comment_type_group_reference=参照
 comment_type_group_label=ラベル
 comment_type_group_milestone=マイルストーン
@@ -726,7 +780,7 @@ add_email_success=新しいメールアドレスを追加しました。
 email_preference_set_success=メール設定を保存しました。
 add_openid_success=新しいOpenIDアドレスを追加しました。
 keep_email_private=メールアドレスを隠す
-keep_email_private_popup=これによりプロフィールでメールアドレスが隠され、Webインターフェースでのプルリクエスト作成やファイル編集でもメールアドレスが隠されます。 プッシュ済みのコミットは変更されません。
+keep_email_private_popup=あなたのプロフィールからメールアドレスが隠され、Webインターフェースを使ったプルリクエスト作成やファイル編集でも、メールアドレスが隠されます。 プッシュ済みのコミットは変更されません。 コミットであなたのアカウントに関連付ける場合は %s を使用してください。
 openid_desc=OpenIDを使うと外部プロバイダーに認証を委任することができます。
 
 manage_ssh_keys=SSHキーの管理
@@ -757,7 +811,6 @@ gpg_invalid_token_signature=入力されたGPG鍵、署名、トークンが合
 gpg_token_required=以下のトークンの署名を入力する必要があります
 gpg_token=トークン
 gpg_token_help=署名はこの方法で生成できます:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Armor形式のGPG署名
 key_signature_gpg_placeholder=先頭は '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=GPG鍵 "%s" を確認しました。
@@ -951,7 +1004,7 @@ fork_branch=フォークにクローンされるブランチ
 all_branches=すべてのブランチ
 fork_no_valid_owners=このリポジトリには有効なオーナーがいないため、フォークできません。
 use_template=このテンプレートを使用
-clone_in_vsc=VSCodeでクローン
+open_with_editor=%s で開く
 download_zip=ZIPファイルをダウンロード
 download_tar=TAR.GZファイルをダウンロード
 download_bundle=バンドルをダウンロード
@@ -967,6 +1020,8 @@ issue_labels_helper=イシューのラベルセットを選択
 license=ライセンス
 license_helper=ライセンス ファイルを選択してください。
 license_helper_desc=ライセンスにより、他人があなたのコードに対して何ができて何ができないのかを規定します。 どれがプロジェクトにふさわしいか迷っていますか? <a target="_blank" rel="noopener noreferrer" href="%s">ライセンス選択サイト</a> も確認してみてください。
+object_format=オブジェクトのフォーマット
+object_format_helper=リポジトリのオブジェクトフォーマット。後で変更することはできません。SHA1 は最も互換性があります。
 readme=README
 readme_helper=READMEファイル テンプレートを選択してください。
 readme_helper_desc=プロジェクトについての説明をひととおり書く場所です。
@@ -984,6 +1039,7 @@ mirror_prune=Prune
 mirror_prune_desc=不要になった古いリモートトラッキング参照を削除
 mirror_interval=ミラー間隔 (有効な時間の単位は'h'、'm'、's')。 定期的な同期を無効にする場合は0。(最小間隔: %s)
 mirror_interval_invalid=ミラー間隔が不正です。
+mirror_sync=前回の同期
 mirror_sync_on_commit=コミットがプッシュされたときに同期
 mirror_address=クローンするURL
 mirror_address_desc=必要な資格情報は「認証」セクションに設定してください。
@@ -1001,6 +1057,7 @@ watchers=ウォッチャー
 stargazers=スターゲイザー
 stars_remove_warning=これを指定すると、このリポジトリのスターはすべて削除されます。
 forks=フォーク
+stars=スター
 reactions_more=さらに %d 件
 unit_disabled=サイト管理者がこのリポジトリセクションを無効にしています。
 language_other=その他
@@ -1032,8 +1089,9 @@ transfer.no_permission_to_reject=この移転を拒否する権限がありま
 desc.private=プライベート
 desc.public=公開
 desc.template=テンプレート
-desc.internal=組織内
+desc.internal=内部
 desc.archived=アーカイブ
+desc.sha256=SHA256
 
 template.items=テンプレート項目
 template.git_content=Gitコンテンツ (デフォルトブランチ)
@@ -1184,6 +1242,8 @@ audio_not_supported_in_browser=このブラウザーはHTML5のaudioタグをサ
 stored_lfs=Git LFSで保管されています
 symbolic_link=シンボリック リンク
 executable_file=実行ファイル
+vendored=ベンダーファイル
+generated=生成ファイル
 commit_graph=コミットグラフ
 commit_graph.select=ブランチを選択
 commit_graph.hide_pr_refs=プルリクエストを非表示
@@ -1247,6 +1307,8 @@ editor.file_editing_no_longer_exists=編集中のファイル "%s" が、もう
 editor.file_deleting_no_longer_exists=削除しようとしたファイル "%s" が、すでにリポジトリ内にありません。
 editor.file_changed_while_editing=あなたが編集を開始したあと、ファイルの内容が変更されました。 <a target="_blank" rel="noopener noreferrer" href="%s">ここをクリック</a>して何が変更されたか確認するか、<strong>もう一度"変更をコミット"をクリック</strong>して上書きします。
 editor.file_already_exists=ファイル "%s" は、このリポジトリに既に存在します。
+editor.commit_id_not_matching=コミットIDが編集を開始したときのIDと一致しません。 パッチ用のブランチにコミットしたあとマージしてください。
+editor.push_out_of_date=このプッシュは最新ではないようです。
 editor.commit_empty_file_header=空ファイルのコミット
 editor.commit_empty_file_text=コミットしようとしているファイルは空です。 続行しますか?
 editor.no_changes_to_show=表示する変更箇所はありません。
@@ -1270,9 +1332,8 @@ commits.desc=ソースコードの変更履歴を参照します。
 commits.commits=コミット
 commits.no_commits=共通のコミットはありません。 "%s" と "%s" の履歴はすべて異なっています。
 commits.nothing_to_compare=二つのブランチは同じ内容です。
-commits.search=コミットの検索…
 commits.search.tooltip=`キーワード "author:"、"committer:"、"after:"、"before:" を付けて指定できます。 例 "revert author:Alice before:2019-01-13"`
-commits.find=検索
+commits.search_branch=このブランチ
 commits.search_all=すべてのブランチ
 commits.author=作成者
 commits.message=メッセージ
@@ -1323,7 +1384,6 @@ projects.type.basic_kanban=基本的なカンバン
 projects.type.bug_triage=バグ トリアージ
 projects.template.desc=テンプレート
 projects.template.desc_helper=開始するプロジェクトテンプレートを選択
-projects.type.uncategorized=未分類
 projects.column.edit=列を編集
 projects.column.edit_title=名称
 projects.column.new_title=名称
@@ -1331,10 +1391,8 @@ projects.column.new_submit=列を作成
 projects.column.new=新しい列
 projects.column.set_default=デフォルトに設定
 projects.column.set_default_desc=この列を未分類のイシューやプルリクエストが入るデフォルトの列にします
-projects.column.unset_default=デフォルトを解除
-projects.column.unset_default_desc=この列からデフォルト列の設定を解除します
 projects.column.delete=列を削除
-projects.column.deletion_desc=プロジェクト列を削除すると、関連するすべてのイシューが '未分類' に移動します。 続行しますか?
+projects.column.deletion_desc=プロジェクト列を削除すると、関連するすべてのイシューがデフォルトの列に移動します。 続行しますか?
 projects.column.color=カラー
 projects.open=オープン
 projects.close=クローズ
@@ -1446,7 +1504,6 @@ issues.filter_sort.moststars=スターが多い順
 issues.filter_sort.feweststars=スターが少ない順
 issues.filter_sort.mostforks=フォークが多い順
 issues.filter_sort.fewestforks=フォークが少ない順
-issues.keyword_search_unavailable=現在キーワード検索は利用できません。 サイト管理者にお問い合わせください。
 issues.action_open=オープン
 issues.action_close=クローズ
 issues.action_label=ラベル
@@ -1503,7 +1560,7 @@ issues.role.member_helper=このユーザーはこのリポジトリを所有し
 issues.role.collaborator=共同作業者
 issues.role.collaborator_helper=このユーザーはリポジトリ上で共同作業するように招待されています。
 issues.role.first_time_contributor=初めての貢献者
-issues.role.first_time_contributor_helper=これは、このユーザーのリポジトリへの最初の貢献です。
+issues.role.first_time_contributor_helper=これは、このユーザーによるリポジトリへの最初の貢献です。
 issues.role.contributor=貢献者
 issues.role.contributor_helper=このユーザーは以前にリポジトリにコミットしています。
 issues.re_request_review=レビューを再依頼
@@ -1698,7 +1755,6 @@ pulls.compare_compare=プル元
 pulls.switch_comparison_type=比較の種類を切り替える
 pulls.switch_head_and_base=ヘッドとベースを切り替える
 pulls.filter_branch=ブランチの絞り込み
-pulls.no_results=結果が見つかりませんでした。
 pulls.show_all_commits=すべてのコミットを表示
 pulls.show_changes_since_your_last_review=前回の自分のレビューからの変更を表示
 pulls.showing_only_single_commit=コミット %[1]s の変更だけを表示しています
@@ -1707,6 +1763,7 @@ pulls.select_commit_hold_shift_for_range=コミットを選択。シフトを押
 pulls.review_only_possible_for_full_diff=すべての差分を表示しているときだけレビューが可能です
 pulls.filter_changes_by_commit=コミットで絞り込み
 pulls.nothing_to_compare=同じブランチ同士のため、 プルリクエストを作成する必要がありません。
+pulls.nothing_to_compare_have_tag=選択したブランチ/タグは同一のものです。
 pulls.nothing_to_compare_and_allow_empty_pr=これらのブランチは内容が同じです。 空のプルリクエストになります。
 pulls.has_pull_request=`同じブランチのプルリクエストはすでに存在します: <a href="%[1]s">%[2]s#%[3]d</a>`
 pulls.create=プルリクエストを作成
@@ -1735,7 +1792,7 @@ pulls.is_checking=マージのコンフリクトを確認中です。 少し待
 pulls.is_ancestor=このブランチは既にターゲットブランチに含まれています。マージするものはありません。
 pulls.is_empty=このブランチの変更は既にターゲットブランチにあります。これは空のコミットになります。
 pulls.required_status_check_failed=いくつかの必要なステータスチェックが成功していません。
-pulls.required_status_check_missing=必要なステータスチェックが見つかりません。
+pulls.required_status_check_missing=必要なチェックがいくつか抜けています。
 pulls.required_status_check_administrator=管理者であるため、このプルリクエストをマージすることは可能です。
 pulls.blocked_by_approvals=このプルリクエストはまだ承認数が足りません。 %[1]d/%[2]dの承認を得ています。
 pulls.blocked_by_rejection=このプルリクエストは公式レビューアにより変更要請されています。
@@ -1765,6 +1822,7 @@ pulls.merge_pull_request=マージコミットを作成
 pulls.rebase_merge_pull_request=リベース後にファストフォワード
 pulls.rebase_merge_commit_pull_request=リベース後にマージコミット作成
 pulls.squash_merge_pull_request=スカッシュコミットを作成
+pulls.fast_forward_only_merge_pull_request=ファストフォワードのみ
 pulls.merge_manually=手動マージ済みにする
 pulls.merge_commit_id=マージコミットID
 pulls.require_signed_wont_sign=ブランチでは署名されたコミットが必須ですが、このマージでは署名がされません
@@ -1901,6 +1959,9 @@ wiki.page_name_desc=この Wiki ページの名前を入力してください。
 wiki.original_git_entry_tooltip=フレンドリーリンクを使用する代わりにオリジナルのGitファイルを表示します。
 
 activity=アクティビティ
+activity.navbar.pulse=Pulse
+activity.navbar.contributors=貢献者
+activity.navbar.recent_commits=最近のコミット
 activity.period.filter_label=期間:
 activity.period.daily=1日
 activity.period.halfweekly=3日
@@ -1966,16 +2027,10 @@ activity.git_stats_and_deletions=、
 activity.git_stats_deletion_1=%d行削除
 activity.git_stats_deletion_n=%d行削除
 
-search=検索
-search.search_repo=リポジトリを検索
-search.type.tooltip=検索タイプ
-search.fuzzy=あいまい
-search.fuzzy.tooltip=検索ワードにおおよそ一致している結果も含めます
-search.match=一致
-search.match.tooltip=検索ワードに一致する結果だけを含めます
-search.results=<a href="%[2]s">%[3]s</a> 内での "%[1]s" の検索結果
-search.code_no_results=検索ワードに一致するソースコードが見つかりません。
-search.code_search_unavailable=現在コード検索は利用できません。 サイト管理者にお問い合わせください。
+contributors.contribution_type.filter_label=実績タイプ:
+contributors.contribution_type.commits=コミット
+contributors.contribution_type.additions=追加
+contributors.contribution_type.deletions=削除
 
 settings=設定
 settings.desc=設定では、リポジトリの設定を管理することができます。
@@ -2002,7 +2057,8 @@ settings.mirror_settings.docs.more_information_if_disabled=プッシュミラー
 settings.mirror_settings.docs.doc_link_title=リポジトリをミラーリングするには?
 settings.mirror_settings.docs.doc_link_pull_section=ドキュメントの「リモートリポジトリからのプル」セクション。
 settings.mirror_settings.docs.pulling_remote_title=リモートリポジトリからのプル
-settings.mirror_settings.mirrored_repository=同期するリポジトリ
+settings.mirror_settings.mirrored_repository=ミラー元のリポジトリ
+settings.mirror_settings.pushed_repository=プッシュ先のリポジトリ
 settings.mirror_settings.direction=方向
 settings.mirror_settings.direction.pull=プル
 settings.mirror_settings.direction.push=プッシュ
@@ -2053,7 +2109,8 @@ settings.pulls.default_delete_branch_after_merge=デフォルトでプルリク
 settings.pulls.default_allow_edits_from_maintainers=デフォルトでメンテナからの編集を許可する
 settings.releases_desc=リリースを有効にする
 settings.packages_desc=リポジトリパッケージレジストリを有効にする
-settings.projects_desc=リポジトリプロジェクトを有効にする
+settings.projects_desc=プロジェクトを有効にする
+settings.projects_mode_all=すべてのプロジェクト
 settings.actions_desc=Actionsを有効にする
 settings.admin_settings=管理者用設定
 settings.admin_enable_health_check=リポジトリのヘルスチェックを有効にする (git fsck)
@@ -2128,7 +2185,6 @@ settings.delete_collaborator=削除
 settings.collaborator_deletion=共同作業者の削除
 settings.collaborator_deletion_desc=共同作業者を削除し、このリポジトリへのアクセス権を取り消します。 続行しますか?
 settings.remove_collaborator_success=共同作業者を削除しました。
-settings.search_user_placeholder=ユーザーを検索…
 settings.org_not_allowed_to_be_collaborator=組織を共同作業者として追加することはできません。
 settings.change_team_access_not_allowed=リポジトリに対するチームアクセス権の変更は、組織のオーナーのみに制限されています。
 settings.team_not_in_organization=チームがリポジトリと同じ組織に属していません。
@@ -2136,7 +2192,6 @@ settings.teams=チーム
 settings.add_team=チームを追加
 settings.add_team_duplicate=チームにはすでにこのリポジトリが登録されています。
 settings.add_team_success=チームがこのリポジトリにアクセスできるようになりました。
-settings.search_team=チームを検索…
 settings.change_team_permission_tip=チームの権限はチーム設定ページで設定されており、リポジトリごとに変更することはできません
 settings.delete_team_tip=このチームはすべてのリポジトリにアクセスでき、削除できません
 settings.remove_team_success=チームのこのリポジトリへのアクセス権を削除しました。
@@ -2289,9 +2344,7 @@ settings.protect_whitelist_committers=ホワイトリストでプッシュを制
 settings.protect_whitelist_committers_desc=ホワイトリストに登録したユーザーまたはチームにのみ、このブランチへのプッシュが許可されます。(強制プッシュ以外)
 settings.protect_whitelist_deploy_keys=プッシュ可能な書き込み権限を持つデプロイキーをホワイトリストに含める。
 settings.protect_whitelist_users=プッシュ・ホワイトリストに含むユーザー:
-settings.protect_whitelist_search_users=ユーザーを検索…
 settings.protect_whitelist_teams=プッシュ・ホワイトリストに含むチーム:
-settings.protect_whitelist_search_teams=チームを検索…
 settings.protect_merge_whitelist_committers=マージ・ホワイトリストを有効にする
 settings.protect_merge_whitelist_committers_desc=ホワイトリストに登録したユーザーまたはチームにだけ、このブランチに対するプルリクエストのマージを許可します。
 settings.protect_merge_whitelist_users=マージ・ホワイトリストに含むユーザー:
@@ -2312,6 +2365,8 @@ settings.protect_approvals_whitelist_users=ホワイトリストに含めるレ
 settings.protect_approvals_whitelist_teams=ホワイトリストに含めるレビューチーム:
 settings.dismiss_stale_approvals=古くなった承認を取り消す
 settings.dismiss_stale_approvals_desc=プルリクエストの内容を変える新たなコミットがブランチにプッシュされた場合、以前の承認を取り消します。
+settings.ignore_stale_approvals=古くなった承認を無視する
+settings.ignore_stale_approvals_desc=古いコミットに対して行われた承認 (古いレビュー) を、PRの承認数にカウントしません。 古いレビューが取り消される場合は関係ありません。
 settings.require_signed_commits=コミット署名必須
 settings.require_signed_commits_desc=署名されていない場合、または署名が検証できなかった場合は、このブランチへのプッシュを拒否します。
 settings.protect_branch_name_pattern=保護ブランチ名のパターン
@@ -2367,6 +2422,7 @@ settings.archive.error=リポジトリのアーカイブ設定でエラーが発
 settings.archive.error_ismirror=ミラーのリポジトリはアーカイブできません。
 settings.archive.branchsettings_unavailable=ブランチ設定は、アーカイブリポジトリでは使用できません。
 settings.archive.tagsettings_unavailable=タグ設定は、アーカイブリポジトリでは使用できません。
+settings.archive.mirrors_unavailable=リポジトリがアーカイブされている場合、ミラーは利用できません。
 settings.unarchive.button=アーカイブ解除
 settings.unarchive.header=このリポジトリをアーカイブ解除
 settings.unarchive.text=リポジトリのアーカイブを解除すると、コミット、プッシュ、新規のイシューやプルリクエストを受け付ける機能が復活します。
@@ -2533,7 +2589,6 @@ branch.default_deletion_failed=ブランチ "%s" はデフォルトブランチ
 branch.restore=ブランチ "%s" の復元
 branch.download=ブランチ "%s" をダウンロード
 branch.rename=ブランチ名 "%s" を変更
-branch.search=ブランチを検索
 branch.included_desc=このブランチはデフォルトブランチに含まれています
 branch.included=埋没
 branch.create_new_branch=このブランチをもとに作成します:
@@ -2565,6 +2620,14 @@ error.csv.too_large=このファイルは大きすぎるため表示できませ
 error.csv.unexpected=このファイルは %d 行目の %d 文字目に予期しない文字が含まれているため表示できません。
 error.csv.invalid_field_count=このファイルは %d 行目のフィールドの数が正しくないため表示できません。
 
+[graphs]
+component_loading=%sを読み込み中...
+component_loading_failed=%sを読み込めませんでした
+component_loading_info=少し時間がかかるかもしれません…
+component_failed_to_load=予期しないエラーが発生しました。
+contributors.what=実績
+recent_commits.what=最近のコミット
+
 [org]
 org_name_holder=組織名
 org_full_name_holder=組織のフルネーム
@@ -2669,7 +2732,6 @@ teams.write_permission_desc=このチームは<strong>書き込み</strong>ア
 teams.admin_permission_desc=このチームは<strong>管理者</strong>アクセス権を持ちます: メンバーはチームリポジトリの読み取り、プッシュ、共同作業者の追加が可能です。
 teams.create_repo_permission_desc=さらに、このチームには<strong>リポジトリの作成</strong>権限が与えられています: メンバーは組織のリポジトリを新たに作成できます。
 teams.repositories=チームのリポジトリ
-teams.search_repo_placeholder=リポジトリを検索…
 teams.remove_all_repos_title=チームリポジトリをすべて除去
 teams.remove_all_repos_desc=チームからすべてのリポジトリを除去します。
 teams.add_all_repos_title=すべてのリポジトリを追加
@@ -2678,6 +2740,7 @@ teams.add_nonexistent_repo=追加しようとしているリポジトリは存
 teams.add_duplicate_users=ユーザーは既にチームのメンバーです。
 teams.repos.none=このチームがアクセスできるリポジトリはありません。
 teams.members.none=このチームにはメンバーがいません。
+teams.members.blocked_user=組織によってブロックされているため、ユーザーを追加できません。
 teams.specific_repositories=指定したリポジトリ
 teams.specific_repositories_helper=メンバーは、明示的にチームへ追加したリポジトリにのみアクセスできます。 これを選択しても、すでに<i>すべてのリポジトリ</i>で追加されたリポジトリは自動的に除去<strong>されません</strong>。
 teams.all_repositories=すべてのリポジトリ
@@ -2691,6 +2754,7 @@ teams.invite.description=下のボタンをクリックしてチームに参加
 
 [admin]
 dashboard=ダッシュボード
+self_check=セルフチェック
 identity_access=アイデンティティとアクセス
 users=ユーザーアカウント
 organizations=組織
@@ -2701,6 +2765,8 @@ integrations=連携
 authentication=認証ソース
 emails=ユーザーメールアドレス
 config=設定
+config_summary=サマリー
+config_settings=設定
 notices=システム通知
 monitor=モニタリング
 first_page=最初
@@ -2710,7 +2776,6 @@ settings=管理設定
 
 dashboard.new_version_hint=Gitea %s が入手可能になりました。 現在実行しているのは %s です。 詳細は <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">ブログ</a> を確認してください。
 dashboard.statistic=サマリー
-dashboard.operations=メンテナンス操作
 dashboard.system_status=システム状況
 dashboard.operation_name=操作の名称
 dashboard.operation_switch=切り替え
@@ -2736,6 +2801,7 @@ dashboard.delete_missing_repos=Gitファイルが存在しないリポジトリ
 dashboard.delete_missing_repos.started=Gitファイルが存在しないリポジトリをすべて削除するタスクを開始しました。
 dashboard.delete_generated_repository_avatars=自動生成したリポジトリアバターを削除
 dashboard.sync_repo_branches=Gitデータからデータベースへ不足しているブランチを同期
+dashboard.sync_repo_tags=Gitデータからデータベースへタグを同期
 dashboard.update_mirrors=ミラーの更新
 dashboard.repo_health_check=全リポジトリのヘルスチェック
 dashboard.check_repo_stats=全リポジトリの統計情報を更新
@@ -2790,6 +2856,7 @@ dashboard.stop_endless_tasks=終わらないタスクを停止
 dashboard.cancel_abandoned_jobs=放置されたままのジョブをキャンセル
 dashboard.start_schedule_tasks=スケジュールタスクを開始
 dashboard.sync_branch.started=ブランチの同期を開始しました
+dashboard.sync_tag.started=タグの同期を開始しました
 dashboard.rebuild_issue_indexer=イシューインデクサーの再構築
 
 users.user_manage_panel=ユーザーアカウント管理
@@ -2875,9 +2942,6 @@ repos.unadopted.no_more=未登録のリポジトリはありません
 repos.owner=オーナー
 repos.name=名称
 repos.private=プライベート
-repos.watches=ウォッチ
-repos.stars=スター
-repos.forks=フォーク
 repos.issues=イシュー
 repos.size=サイズ
 repos.lfs_size=LFSサイズ
@@ -2897,12 +2961,12 @@ packages.size=サイズ
 packages.published=配布
 
 defaulthooks=デフォルトWebhook
-defaulthooks.desc=Webhookは、特定のGiteaイベントのトリガーが発生した際に、自動的にHTTP POSTリクエストをサーバーへ送信するものです。 ここで定義されたWebhookはデフォルトとなり、全ての新規リポジトリにコピーされます。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。
+defaulthooks.desc=Webhookは、特定のGiteaイベントが発生したときに、サーバーにHTTP POSTリクエストを自動的に送信するものです。 ここで定義したWebhookはデフォルトとなり、全ての新規リポジトリにコピーされます。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。
 defaulthooks.add_webhook=デフォルトWebhookの追加
 defaulthooks.update_webhook=デフォルトWebhookの更新
 
 systemhooks=システムWebhook
-systemhooks.desc=Webhookは、特定のGiteaイベントのトリガーが発生した際に、自動的にHTTP POSTリクエストをサーバーへ送信するものです。 ここで定義したWebhookはシステム内のすべてのリポジトリで呼び出されます。 そのため、パフォーマンスに及ぼす影響を考慮したうえで設定してください。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。
+systemhooks.desc=Webhookは、特定のGiteaイベントが発生したときに、サーバーにHTTP POSTリクエストを自動的に送信するものです。 ここで定義したWebhookは、システム内のすべてのリポジトリで呼び出されます。 そのため、パフォーマンスに及ぼす影響を考慮したうえで設定してください。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。
 systemhooks.add_webhook=システムWebhookを追加
 systemhooks.update_webhook=システムWebhookを更新
 
@@ -3002,7 +3066,7 @@ auths.tip.nextcloud=新しいOAuthコンシューマーを、インスタンス
 auths.tip.dropbox=新しいアプリケーションを https://www.dropbox.com/developers/apps から登録してください。
 auths.tip.facebook=新しいアプリケーションを https://developers.facebook.com/apps で登録し、"Facebook Login"を追加してください。
 auths.tip.github=新しいOAuthアプリケーションを https://github.com/settings/applications/new から登録してください。
-auths.tip.gitlab=新しいアプリケーションを https://gitlab.com/profile/applications から登録してください。
+auths.tip.gitlab_new=新しいアプリケーションを https://gitlab.com/-/profile/applications から登録してください。
 auths.tip.google_plus=OAuth2クライアント資格情報を、Google APIコンソール https://console.developers.google.com/ から取得してください。
 auths.tip.openid_connect=OpenID Connect DiscoveryのURL (<server>/.well-known/openid-configuration) をエンドポイントとして指定してください
 auths.tip.twitter=https://dev.twitter.com/apps へアクセスしてアプリケーションを作成し、“Allow this application to be used to Sign in with Twitter”オプションを有効にしてください。
@@ -3138,6 +3202,7 @@ config.picture_config=画像とアバターの設定
 config.picture_service=画像サービス
 config.disable_gravatar=Gravatarが無効
 config.enable_federated_avatar=フェデレーテッド・アバター有効
+config.open_with_editor_app_help=クローンメニューの「~で開く」に表示するエディタ。 空白のままにするとデフォルトが使用されます。 展開するとデフォルトを確認できます。
 
 config.git_config=Git設定
 config.git_disable_diff_highlight=Diffのシンタックスハイライトが無効
@@ -3216,6 +3281,13 @@ notices.desc=説明
 notices.op=操作
 notices.delete_success=システム通知を削除しました。
 
+self_check.no_problem_found=今のところ問題は見つかっていません。
+self_check.database_collation_mismatch=データベースに想定される照合順序: %s
+self_check.database_collation_case_insensitive=データベースは照合順序 %s を使用しており、大文字小文字を区別しません。 Giteaはその照合順序でも動作するかもしれませんが、まれに期待どおり動作しないケースがあるかもしれません。
+self_check.database_inconsistent_collation_columns=データベースは照合順序 %s を使用していますが、以下のカラムはそれと一致しない照合順序を使用しており、予期せぬ問題を引き起こす可能性があります。
+self_check.database_fix_mysql=MySQL/MariaDBユーザーの方は、"gitea doctor convert" コマンドを使用することで、照合順序の問題を修正できます。 また、"ALTER ... COLLATE ..." のSQLを手で実行しても修正することができます。
+self_check.database_fix_mssql=MSSQLユーザーの方は、問題を修正するには今のところ "ALTER ... COLLATE ..." のSQLを手で実行するしかありません。
+
 [action]
 create_repo=がリポジトリ <a href="%s">%s</a> を作成しました
 rename_repo=がリポジトリ名を <code>%[1]s</code> から <a href="%[2]s">%[3]s</a> へ変更しました
@@ -3270,9 +3342,9 @@ raw_seconds=秒
 raw_minutes=分
 
 [dropzone]
-default_message=ここにファイルをドロップまたはクリックしてアップロードします。
+default_message=ファイルをここにドロップ、またはここをクリックしてアップロード
 invalid_input_type=この種類のファイルはアップロードできません。
-file_too_big=アップロードされたファイルのサイズ ({{filesize}} MB) が最大サイズ ({{maxFilesize}} MB) を超えています。
+file_too_big=アップロードされたファイルのサイズ ({{filesize}} MB) は、最大サイズ ({{maxFilesize}} MB) を超えています。
 remove_file=ファイル削除
 
 [notification]
@@ -3297,7 +3369,7 @@ error.no_committer_account=コミッターのメールアドレスに対応す
 error.no_gpg_keys_found=この署名に対応する既知のキーがデータベースに存在しません
 error.not_signed_commit=署名されたコミットではありません
 error.failed_retrieval_gpg_keys=コミッターのアカウントに登録されたキーを取得できませんでした
-error.probable_bad_signature=警告! このIDの鍵はデータベースに登録されていますが、その鍵でコミットの検証が通りません! これは疑わしいコミットです。
+error.probable_bad_signature=警告! このIDに該当する鍵がデータベースにありますが、コミットの検証が通りません! これは疑わしいコミットです。
 error.probable_bad_default_signature=警告! これはデフォルト鍵のIDですが、デフォルト鍵ではコミットの検証が通りません! これは疑わしいコミットです。
 
 [units]
@@ -3310,7 +3382,7 @@ title=パッケージ
 desc=リポジトリ パッケージを管理します。
 empty=パッケージはまだありません。
 empty.documentation=パッケージレジストリの詳細については、 <a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a> を参照してください。
-empty.repo=パッケージはアップロードしたけども、ここに表示されない? <a href="%[1]s">パッケージ設定</a>を開いて、パッケージをこのリポジトリにリンクしてください。
+empty.repo=パッケージはアップロード済みで、ここに表示されていないですか? <a href="%[1]s">パッケージ設定</a>を開いて、パッケージをこのリポジトリにリンクしてください。
 registry.documentation=%sレジストリの詳細については、 <a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a> を参照してください。
 filter.type=タイプ
 filter.type.all=すべて
@@ -3400,6 +3472,9 @@ rpm.registry=このレジストリをコマンドラインからセットアッ
 rpm.distros.redhat=RedHat系ディストリビューションの場合
 rpm.distros.suse=SUSE系ディストリビューションの場合
 rpm.install=パッケージをインストールするには、次のコマンドを実行します:
+rpm.repository=リポジトリ情報
+rpm.repository.architectures=Architectures
+rpm.repository.multiple_groups=このパッケージは複数のグループで利用可能です。
 rubygems.install=gem を使用してパッケージをインストールするには、次のコマンドを実行します:
 rubygems.install2=または Gemfile に追加します:
 rubygems.dependencies.runtime=実行用依存関係
@@ -3526,14 +3601,15 @@ runs.scheduled=スケジュール済み
 runs.pushed_by=pushed by
 runs.invalid_workflow_helper=ワークフロー設定ファイルは無効です。あなたの設定ファイルを確認してください: %s
 runs.no_matching_online_runner_helper=ラベルに一致するオンラインのランナーが見つかりません: %s
+runs.no_job_without_needs=ワークフローには依存関係のないジョブが少なくとも1つ含まれている必要があります。
 runs.actor=アクター
 runs.status=ステータス
 runs.actors_no_select=すべてのアクター
 runs.status_no_select=すべてのステータス
 runs.no_results=一致する結果はありません。
 runs.no_workflows=ワークフローはまだありません。
-runs.no_workflows.quick_start=Gitea Action の始め方がわからない? <a target="_blank" rel="noopener noreferrer" href="%s">クイックスタートガイド</a>をご覧ください。
-runs.no_workflows.documentation=Gitea Action の詳細については、<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を参照してください。
+runs.no_workflows.quick_start=Gitea Actions の始め方がわからない? では<a target="_blank" rel="noopener noreferrer" href="%s">クイックスタートガイド</a>をご覧ください。
+runs.no_workflows.documentation=Gitea Actions の詳細については、<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を参照してください。
 runs.no_runs=ワークフローはまだ実行されていません。
 runs.empty_commit_message=(空のコミットメッセージ)
 
@@ -3552,7 +3628,7 @@ variables.none=変数はまだありません。
 variables.deletion=変数を削除
 variables.deletion.description=変数の削除は恒久的で元に戻すことはできません。 続行しますか?
 variables.description=変数は特定のActionsに渡されます。 それ以外で読み出されることはありません。
-variables.id_not_exist=idが%dの変数は存在しません。
+variables.id_not_exist=IDが%dの変数は存在しません。
 variables.edit=変数の編集
 variables.deletion.failed=変数を削除できませんでした。
 variables.deletion.success=変数を削除しました。
diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini
index 1c79ee6bc7..3e9679575c 100644
--- a/options/locale/locale_ko-KR.ini
+++ b/options/locale/locale_ko-KR.ini
@@ -84,6 +84,12 @@ concept_user_organization=조직
 
 name=이름
 
+filter.is_template=템플릿
+filter.private=비공개
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -199,7 +205,6 @@ collaborative_repos=협업 저장소
 my_orgs=내 조직
 my_mirrors=내 미러 저장소들
 view_home=%s 보기
-search_repos=저장소 찾기..
 
 
 show_private=비공개
@@ -210,12 +215,7 @@ issues.in_your_repos=당신의 저장소에
 repos=저장소
 users=유저
 organizations=조직
-search=검색
 code=코드
-repo_no_results=일치하는 레포지토리가 없습니다.
-user_no_results=일치하는 사용자가 없습니다.
-org_no_results=일치하는 조직이 없습니다.
-code_no_results=검색어와 일치하는 소스코드가 없습니다.
 
 [auth]
 create_new_account=계정 등록
@@ -226,7 +226,6 @@ disable_register_mail=계정 등록을 위한 이메일 검증이 비활성화 
 forgot_password_title=비밀번호 찾기
 forgot_password=비밀번호를 잊으셨나요?
 sign_up_now=계정이 필요하신가요? 지금 가입하세요.
-confirmation_mail_sent_prompt=새로운 확인 메일이 <b>%s</b>로 전송되었습니다. 받은 편지함으로 도착한 메일을 %s 안에 확인해서 등록 절차를 완료하십시오.
 must_change_password=비밀번호를 변경하세요.
 allow_password_change=사용자에게 비밀번호 변경을 요청 (권장됨)
 reset_password_mail_sent_prompt=확인 메일이 <b>%s</b>로 전송되었습니다. 받은 편지함으로 도착한 메일을 %s 안에 확인해서 비밀번호 찾기 절차를 완료하십시오.
@@ -349,6 +348,7 @@ auth_failed=인증 실패: %v
 
 target_branch_not_exist=대상 브랜치가 존재하지 않습니다.
 
+
 [user]
 change_avatar=아바타 변경
 repositories=저장소
@@ -362,6 +362,7 @@ unfollow=추적해제
 user_bio=소개
 
 
+
 [settings]
 profile=프로필
 account=계정
@@ -660,8 +661,6 @@ editor.add_subdir=경로 추가...
 
 commits.desc=소스 코드 변경 내역 탐색
 commits.commits=커밋
-commits.search=커밋 찾기...
-commits.find=검색
 commits.search_all=모든 브랜치
 commits.author=작성자
 commits.message=메시지
@@ -843,7 +842,6 @@ pulls.compare_changes=새 풀 리퀘스트
 pulls.compare_base=병합하기
 pulls.compare_compare=다음으로부터 풀
 pulls.filter_branch=Filter Branch
-pulls.no_results=결과 없음
 pulls.create=풀 리퀘스트 생성
 pulls.title_desc="<code>%[2]s</code> 에서 <code id=\"branch_target\">%[3]s</code> 로 %[1]d commits 를 머지하려 합니다"
 pulls.merged_title_desc=<code>%[2]s</code> 에서 <code>%[3]s</code> 로 %[1]d commits 를 머지했습니다 %[4]s
@@ -949,10 +947,7 @@ activity.title.releases_n=%d 개의 릴리즈
 activity.title.releases_published_by=%s 가 %s 에 의하여 배포되었습니다.
 activity.published_release_label=배포됨
 
-search=검색
-search.search_repo=저장소 검색
-search.results="<a href=\"%s\">%s</a> 에서 \"%s\" 에 대한 검색 결과"
-search.code_no_results=검색어와 일치하는 소스코드가 없습니다.
+contributors.contribution_type.commits=커밋
 
 settings=설정
 settings.desc=설정은 저장소 설정을 관리할 수 있습니다.
@@ -1013,7 +1008,6 @@ settings.add_collaborator=새 공동작업자 추가
 settings.add_collaborator_success=공동작업자가 추가 되었습니다.
 settings.delete_collaborator=제거
 settings.collaborator_deletion=공동작업자 삭제
-settings.search_user_placeholder=사용자 검색...
 settings.teams=팀
 settings.add_webhook=Webhook 추가
 settings.webhook_deletion=Webhook 삭제
@@ -1087,8 +1081,6 @@ settings.branch_protection='<b>%s</b>' 브랜치 보호
 settings.protect_this_branch=브랜치 보호 활성화
 settings.protect_disable_push=푸시 끄기
 settings.protect_enable_push=푸시 켜기
-settings.protect_whitelist_search_users=사용자 찾기...
-settings.protect_whitelist_search_teams=팀 찾기...
 settings.protect_merge_whitelist_committers=머지 화이트리스트 활성화
 settings.protect_required_approvals=필요한 승인:
 settings.protect_approvals_whitelist_users=화이트리스트된 리뷰어:
@@ -1161,6 +1153,8 @@ topic.count_prompt=25개 이상의 토픽을 선택하실 수 없습니다.
 
 
 
+[graphs]
+
 [org]
 org_name_holder=조직 이름
 org_full_name_holder=조직 전체 이름
@@ -1221,7 +1215,6 @@ teams.add_team_member=팀 구성원 추가
 teams.delete_team_title=팀 삭제
 teams.delete_team_success=팀이 삭제되었습니다.
 teams.repositories=팀 저장소
-teams.search_repo_placeholder=저장소 찾기...
 teams.add_duplicate_users=사용자가 이미 팀 멤버입니다.
 teams.members.none=이 팀에 멤버가 없습니다.
 
@@ -1232,6 +1225,8 @@ organizations=조직
 repositories=저장소
 authentication=인증 소스
 config=설정
+config_summary=요약
+config_settings=설정
 notices=시스템 공지
 monitor=모니터링
 first_page=처음
@@ -1318,9 +1313,6 @@ repos.repo_manage_panel=저장소 관리
 repos.owner=소유자
 repos.name=이름
 repos.private=비공개
-repos.watches=지켜보기
-repos.stars=별
-repos.forks=포크
 repos.issues=이슈
 repos.size=크기
 
@@ -1521,6 +1513,7 @@ notices.desc=설명
 notices.op=일.
 notices.delete_success=시스템 알림이 삭제되었습니다.
 
+
 [action]
 create_repo=저장소를 만들었습니다. <a href="%s">%s</a>
 rename_repo=<code>%[1]s에서</code>에서 <a href="%[2]s"> %[3]s</a>으로 저장소 이름을 바꾸었습니다.
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index e275b02ba0..9a15090012 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -17,10 +17,11 @@ template=Sagatave
 language=Valoda
 notifications=Paziņojumi
 active_stopwatch=Aktīvā laika uzskaite
+tracked_time_summary=Izsekojamā laika apkopojums, kas ir balstīts uz pieteikumu saraksta atlasi
 create_new=Izveidot…
 user_profile_and_more=Profils un iestatījumi…
 signed_in_as=Pieteicies kā
-enable_javascript=Šai lapas darbībai ir nepieciešams JavaScript.
+enable_javascript=Šai tīmekļvietnei ir nepieciešams JavaScript.
 toc=Satura rādītājs
 licenses=Licences
 return_to_gitea=Atgriezties Gitea
@@ -40,12 +41,12 @@ webauthn_sign_in=Nospiediet pogu uz drošības atslēgas. Ja tai nav pogas, izņ
 webauthn_press_button=Nospiediet drošības atslēgas pogu…
 webauthn_use_twofa=Izmantot divfaktoru kodu no tālruņa
 webauthn_error=Nevar nolasīt drošības atslēgu.
-webauthn_unsupported_browser=Jūsu pārlūkprogramma neatbalsta WebAuthn standartu.
+webauthn_unsupported_browser=Jūsu pārlūks neatbalsta WebAuthn standartu.
 webauthn_error_unknown=Notikusi nezināma kļūda. Atkārtojiet darbību vēlreiz.
-webauthn_error_insecure=WebAuthn atbalsta tikai drošus savienojumus ar serveri
-webauthn_error_unable_to_process=Serveris nevar apstrādāt Jūsu pieprasījumu.
+webauthn_error_insecure=`WebAuthn atbalsta tikai drošus savienojumus. Pārbaudīšanai ar HTTP var izmantot izcelsmi "localhost" vai "127.0.0.1"`
+webauthn_error_unable_to_process=Serveris nevarēja apstrādāt pieprasījumu.
 webauthn_error_duplicated=Drošības atslēga nav atļauta šim pieprasījumam. Pārliecinieties, ka šī atslēga jau nav reģistrēta.
-webauthn_error_empty=Norādiet atslēgas nosaukumu.
+webauthn_error_empty=Jānorāda šīs atslēgas nosaukums.
 webauthn_error_timeout=Iestājusies noildze, mēģinot, nolasīt atslēgu. Pārlādējiet lapu un mēģiniet vēlreiz.
 webauthn_reload=Pārlādēt
 
@@ -60,11 +61,11 @@ new_org=Jauna organizācija
 new_project=Jauns projekts
 new_project_column=Jauna kolonna
 manage_org=Pārvaldīt organizācijas
-admin_panel=Lapas administrēšana
+admin_panel=Vietnes administrēšana
 account_settings=Konta iestatījumi
 settings=Iestatījumi
 your_profile=Profils
-your_starred=Atzīmēts ar zvaigznīti
+your_starred=Pievienots izlasē
 your_settings=Iestatījumi
 
 all=Visi
@@ -90,9 +91,11 @@ remove=Noņemt
 remove_all=Noņemt visus
 remove_label_str=`Noņemt ierakstu "%s"`
 edit=Labot
+view=Skatīt
 
 enabled=Iespējots
 disabled=Atspējots
+locked=Slēgts
 
 copy=Kopēt
 copy_url=Kopēt saiti
@@ -109,6 +112,7 @@ loading=Notiek ielāde…
 
 error=Kļūda
 error404=Lapa, ko vēlaties atvērt, <strong>neeksistē</strong> vai arī <strong>Jums nav tiesības</strong> to aplūkot.
+go_back=Atgriezties
 
 never=Nekad
 unknown=Nezināms
@@ -130,12 +134,22 @@ concept_user_organization=Organizācija
 show_timestamps=Rādīt laika zīmogus
 show_log_seconds=Rādīt sekundes
 show_full_screen=Atvērt pilnā logā
+download_logs=Lejupielādēt žurnālus
 
 confirm_delete_selected=Apstiprināt, lai izdzēstu visus atlasītos vienumus?
 
 name=Nosaukums
 value=Vērtība
 
+filter=Filtrs
+filter.is_archived=Arhivētie
+filter.is_template=Sagatave
+filter.public=Publiska
+filter.private=Privāts
+
+
+[search]
+
 [aria]
 navbar=Navigācijas josla
 footer=Kājene
@@ -170,6 +184,7 @@ string.desc=Z - A
 
 [error]
 occurred=Radusies kļūda
+report_message=Ja ir pārliecība, ka šī ir Gitea nepilnība, lūgums pārbaudīt <a href="https://github.com/go-gitea/gitea/issues" target="_blank">GitHub</a>, vai tā jau nav zināma, vai izveidot jaunu pieteikumu, ja nepieciešams.
 missing_csrf=Kļūdains pieprasījums: netika iesūtīta drošības pilnvara
 invalid_csrf=Kļūdains pieprasījums: iesūtīta kļūdaina drošības pilnvara
 not_found=Pieprasītie dati netika atrasti.
@@ -178,6 +193,7 @@ network_error=Tīkla kļūda
 [startpage]
 app_desc=Viegli uzstādāms Git serviss
 install=Vienkārši instalējams
+install_desc=Vienkārši <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">jāpalaiž izpildāmais fails</a> vajadzīgajai platformai, jāizmanto <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a>, vai jāiegūst <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">pakotne</a>.
 platform=Pieejama dažādām platformām
 platform_desc=Gitea iespējams uzstādīt jebkur, kam <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> var nokompilēt: Windows, macOS, Linux, ARM utt. Izvēlies to, kas tev patīk!
 lightweight=Viegla
@@ -222,6 +238,7 @@ repo_path_helper=Git repozitoriji tiks glabāti šajā direktorijā.
 lfs_path=Git LFS glabāšanas vieta
 lfs_path_helper=Faili, kas pievienoti Git LFS, tiks glabāti šajā direktorijā. Atstājiet tukšu, lai atspējotu.
 run_user=Izpildes lietotājs
+run_user_helper=Operētājsistēms lietotājs, ar kuru tiks palaists Gitea. Jāņem vērā, ka šim lietotājam ir jābūt piekļuvei repozitorija atrašanās vietai.
 domain=Servera domēns
 domain_helper=Domēns vai servera adrese.
 ssh_port=SSH servera ports
@@ -293,6 +310,8 @@ invalid_password_algorithm=Kļūdaina paroles jaucējfunkcija
 password_algorithm_helper=Norādiet paroles jaucējalgoritmu. Algoritmi atšķirās pēc prasībām pret resursiem un stipruma. Argon2 algoritms ir drošs, bet tam nepieciešams daudz operatīvās atmiņas, līdz ar ko tas var nebūt piemērots sistēmām ar maz pieejamajiem resursiem.
 enable_update_checker=Iespējot jaunu versiju paziņojumus
 enable_update_checker_helper=Periodiski pārbaudīt jaunu version pieejamību, izgūstot datus no gitea.io.
+env_config_keys=Vides konfigurācija
+env_config_keys_prompt=Šie vides mainīgie tiks pielietoti arī konfigurācijas failā:
 
 [home]
 uname_holder=Lietotājvārds vai e-pasts
@@ -304,7 +323,6 @@ collaborative_repos=Sadarbības repozitoriji
 my_orgs=Manas organizācijas
 my_mirrors=Mani spoguļi
 view_home=Skatīties %s
-search_repos=Meklēt repozitoriju…
 filter=Citi filtri
 filter_by_team_repositories=Filtrēt pēc komandas repozitorijiem
 feed_of=`"%s" plūsma`
@@ -325,20 +343,8 @@ issues.in_your_repos=Jūsu repozitorijos
 repos=Repozitoriji
 users=Lietotāji
 organizations=Organizācijas
-search=Meklēt
 go_to=Iet uz
 code=Kods
-search.type.tooltip=Meklēšanas veids
-search.fuzzy=Aptuveni
-search.fuzzy.tooltip=Iekļaut meklēšanas rezultātos arī aptuvenas sakritības
-search.match=Precīzi
-search.match.tooltip=Iekļaut meklēšanas rezultātos tikai precīzas sakritības
-code_search_unavailable=Pašlaik koda meklēšana nav pieejama. Sazinieties ar lapas administratoru.
-repo_no_results=Netika atrasts neviens repozitorijs, kas atbilstu kritērijiem.
-user_no_results=Netika atrasts neviens lietotājs, kas atbilstu kritērijiem.
-org_no_results=Netika atrasta neviena organizācija, kas atbilstu kritērijiem.
-code_no_results=Netika atrasts pirmkods, kas atbilstu kritērijiem.
-code_search_results=`Meklēšanas rezultāti "%s"`
 code_last_indexed_at=Pēdējo reizi indeksēts %s
 relevant_repositories_tooltip=Repozitoriju, kas ir atdalīti vai kuriem nav tēmas, ikonas un apraksta ir paslēpti.
 relevant_repositories=Tikai būtiskie repozitoriji tiek rādīti, <a href="%s">pārādīt nefiltrētus rezultātus</a>.
@@ -351,10 +357,11 @@ disable_register_prompt=Reģistrācija ir atspējota. Lūdzu, sazinieties ar vie
 disable_register_mail=Reģistrācijas e-pasta apstiprināšana ir atspējota.
 manual_activation_only=Sazinieties ar lapas administratoru, lai pabeigtu konta aktivizāciju.
 remember_me=Atcerēties šo ierīci
+remember_me.compromised=Pieteikšanās pilnvara vairs nav derīga, kas var norādīt uz ļaunprātīgām darbībām kontā. Lūgums pārbaudīt, vai kontā nav neparastu darbību.
 forgot_password_title=Aizmirsu paroli
 forgot_password=Aizmirsi paroli?
 sign_up_now=Nepieciešams konts? Reģistrējies tagad.
-confirmation_mail_sent_prompt=Jauns apstiprināšanas e-pasts ir nosūtīts uz <b>%s</b>, pārbaudies savu e-pasta kontu tuvāko %s laikā, lai pabeigtu reģistrācijas procesu.
+sign_up_successful=Konts tika veiksmīgi izveidots. Laipni lūdzam!
 must_change_password=Mainīt paroli
 allow_password_change=Pieprasīt lietotājam mainīt paroli (ieteicams)
 reset_password_mail_sent_prompt=Apstiprināšanas e-pasts tika nosūtīts uz <b>%s</b>. Pārbaudiet savu e-pasta kontu tuvāko %s laikā, lai pabeigtu paroles atjaunošanas procesu.
@@ -369,6 +376,7 @@ email_not_associate=Šī e-pasta adrese nav saistīta ar nevienu kontu.
 send_reset_mail=Nosūtīt paroles atjaunošanas e-pastu
 reset_password=Paroles atjaunošana
 invalid_code=Jūsu apstiprināšanas kodam ir beidzies derīguma termiņš vai arī tas ir nepareizs.
+invalid_code_forgot_password=Apliecinājuma kods ir nederīgs vai tā derīgums ir beidzies. Nospiediet <a href="%s">šeit</a>, lai uzsāktu jaunu sesiju.
 invalid_password=Jūsu parole neatbilst parolei, kas tika ievadīta veidojot so kontu.
 reset_password_helper=Atjaunot paroli
 reset_password_wrong_user=Jūs esat pieteicies kā %s, bet konta atkopšanas saite ir paredzēta lietotājam %s
@@ -396,6 +404,7 @@ openid_connect_title=Pievienoties jau esošam kontam
 openid_connect_desc=Izvēlētais OpenID konts sistēmā netika atpazīts, bet Jūs to varat piesaistīt esošam kontam.
 openid_register_title=Izveidot jaunu kontu
 openid_register_desc=Izvēlētais OpenID konts sistēmā netika atpazīts, bet Jūs to varat piesaistīt esošam kontam.
+openid_signin_desc=Jāievada OpenID URI. Piemēram, anna.openid.example.org vai https://openid.example.org/anna.
 disable_forgot_password_mail=Konta atjaunošana ir atspējota, jo nav uzstādīti e-pasta servera iestatījumi. Sazinieties ar lapas administratoru.
 disable_forgot_password_mail_admin=Kontu atjaunošana ir pieejama tikai, ja ir veikta e-pasta servera iestatījumu konfigurēšana. Norādiet e-pasta servera iestatījumus, lai iespējotu kontu atjaunošanu.
 email_domain_blacklisted=Nav atļauts reģistrēties ar šādu e-pasta adresi.
@@ -405,7 +414,9 @@ authorize_application_created_by=Šo lietotni izveidoja %s.
 authorize_application_description=Ja piešķirsiet tiesības, tā varēs piekļūt un mainīt Jūsu konta informāciju, ieskaitot privātos repozitorijus un organizācijas.
 authorize_title=Autorizēt "%s" piekļuvi jūsu kontam?
 authorization_failed=Autorizācija neizdevās
+authorization_failed_desc=Autentifikācija neizdevās, jo tika veikts kļūdains pieprasījums. Sazinieties ar lietojumprogrammas, ar kuru mēģinājāt autentificēties, uzturētāju.
 sspi_auth_failed=SSPI autentifikācija neizdevās
+password_pwned=Izvēlētā parole ir <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">nozagto paroļu sarakstā</a>, kas iepriekš ir atklāts publiskās datu noplūdēs. Lūgums mēģināt vēlreiz ar citu paroli un apsvērt to nomainīt arī citur.
 password_pwned_err=Neizdevās pabeigt pieprasījumu uz HaveIBeenPwned
 
 [mail]
@@ -420,6 +431,7 @@ activate_account.text_1=Sveiki <b>%[1]s</b>, esat reģistrējies %[2]s!
 activate_account.text_2=Nospiediet uz saites, lai aktivizētu savu kontu lapā <b>%s</b>:
 
 activate_email=Apstipriniet savu e-pasta adresi
+activate_email.title=%s, apstipriniet savu e-pasta adresi
 activate_email.text=Nospiediet uz saites, lai apstiprinātu savu e-pasta adresi lapā <b>%s</b>:
 
 register_notify=Laipni lūdzam Gitea
@@ -571,6 +583,7 @@ org_still_own_packages=Šai organizācijai pieder viena vai vārākas pakotnes,
 
 target_branch_not_exist=Mērķa atzars neeksistē
 
+
 [user]
 change_avatar=Mainīt profila attēlu…
 joined_on=Pievienojās %s
@@ -589,11 +602,14 @@ user_bio=Biogrāfija
 disabled_public_activity=Šis lietotājs ir atslēdzies iespēju aplūkot tā aktivitāti.
 email_visibility.limited=E-pasta adrese ir redzama visiem autentificētajiem lietotājiem
 email_visibility.private=E-pasta adrese ir redzama tikai administratoriem
+show_on_map=Rādīt šo vietu kartē
+settings=Lietotāja iestatījumi
 
 form.name_reserved=Lietotājvārdu "%s" nedrīkst izmantot.
 form.name_pattern_not_allowed=Lietotājvārds "%s" nav atļauts.
 form.name_chars_not_allowed=Lietotāja vārds "%s" satur neatļautus simbolus.
 
+
 [settings]
 profile=Profils
 account=Konts
@@ -610,9 +626,13 @@ delete=Dzēst kontu
 twofa=Divfaktoru autentifikācija
 account_link=Saistītie konti
 organization=Organizācijas
+uid=UID
 webauthn=Drošības atslēgas
 
 public_profile=Publiskais profils
+biography_placeholder=Pastāsti mums mazliet par sevi! (Var izmantot Markdown)
+location_placeholder=Kopīgot savu aptuveno atrašanās vietu ar citiem
+profile_desc=Norādīt, kā profils tiek attēlots citiem lietotājiem. Primārā e-pasta adrese tiks izmantota paziņojumiem, paroles atjaunošanai un Git tīmekļa darbībām.
 password_username_disabled=Ne-lokāliem lietotājiem nav atļauts mainīt savu lietotāja vārdu. Sazinieties ar sistēmas administratoru, lai uzzinātu sīkāk.
 full_name=Pilns vārds
 website=Mājas lapa
@@ -624,6 +644,8 @@ update_language_not_found=Valoda "%s" nav pieejama.
 update_language_success=Valoda tika nomainīta.
 update_profile_success=Jūsu profila informācija tika saglabāta.
 change_username=Lietotājvārds mainīts.
+change_username_prompt=Piezīme: lietotājvārda mainīšana maina arī konta URL.
+change_username_redirect_prompt=Iepriekšējais lietotājvārds tiks pārvirzīts, kamēr neviens cits to neizmanto.
 continue=Turpināt
 cancel=Atcelt
 language=Valoda
@@ -648,6 +670,7 @@ comment_type_group_project=Projektus
 comment_type_group_issue_ref=Problēmu atsauces
 saved_successfully=Iestatījumi tika veiksmīgi saglabati.
 privacy=Privātums
+keep_activity_private=Profila lapā paslēpt notikumus
 keep_activity_private_popup=Savu aktivitāti redzēsiet tikai Jūs un administratori
 
 lookup_avatar_by_mail=Meklēt profila bildes pēc e-pasta
@@ -657,12 +680,14 @@ choose_new_avatar=Izvēlēties jaunu profila attēlu
 update_avatar=Saglabāt profila bildi
 delete_current_avatar=Dzēst pašreizējo profila bildi
 uploaded_avatar_not_a_image=Augšupielādētais fails nav attēls.
+uploaded_avatar_is_too_big=Augšupielādētā faila izmērs (%d KiB) pārsniedz pieļaujamo izmēru (%d KiB).
 update_avatar_success=Profila attēls tika saglabāts.
 update_user_avatar_success=Lietotāja profila attēls tika atjaunots.
 
 change_password=Mainīt paroli
 old_password=Pašreizējā parole
 new_password=Jauna parole
+retype_new_password=Apstiprināt jauno paroli
 password_incorrect=Ievadīta nepareiza pašreizējā parole.
 change_password_success=Parole tika veiksmīgi nomainīta. Tagad varat pieteikties ar jauno paroli.
 password_change_disabled=Ārējie konti nevar mainīt paroli, izmantojot, Gitea saskarni.
@@ -671,6 +696,7 @@ emails=E-pasta adreses
 manage_emails=Pārvaldīt e-pasta adreses
 manage_themes=Izvēlieties noklusējuma motīvu
 manage_openid=Pārvaldīt OpenID adreses
+email_desc=Primārā e-pasta adrese tiks izmantota paziņojumiem, paroļu atjaunošanai un, ja tā nav paslēpta, Git tīmekļa darbībām.
 theme_desc=Šis būs noklusējuma motīvs visiem lietotājiem.
 primary=Primārā
 activated=Aktivizēts
@@ -678,6 +704,7 @@ requires_activation=Nepieciešams aktivizēt
 primary_email=Uzstādīt kā primāro
 activate_email=Nosūtīt aktivizācijas e-pastu
 activations_pending=Gaida aktivizāciju
+can_not_add_email_activations_pending=Ir nepabeigta aktivizācija. Pēc dažām minūtēm mēģiniet vēlreiz, ja ir vēlme pievienot jaunu e-pasta adresi.
 delete_email=Noņemt
 email_deletion=Dzēst e-pasta adresi
 email_deletion_desc=E-pasta adrese un ar to saistītā informācija tiks dzēsta no šī konta. Git revīzijas ar šo e-pasta adresi netiks mainītas. Vai turpināt?
@@ -696,6 +723,7 @@ add_email_success=Jūsu jaunā e-pasta adrese tika veiksmīgi pievienota.
 email_preference_set_success=E-pasta izvēle tika veiksmīgi saglabāta.
 add_openid_success=Jūsu jaunā OpenID adrese tika veiksmīgi pievienota.
 keep_email_private=Paslēpt e-pasta adresi
+keep_email_private_popup=Šis profilā paslēps e-pasta adresi, kā arī tad, kad tiks veikts izmaiņu pieprasījums vai tīmekļa saskarnē labota datne. Aizgādātie iesūtījumi netiks pārveidoti. Revīzijās jāizmanto %s, lai sasaistītu tos ar kontu.
 openid_desc=Jūsu OpenID adreses ļauj autorizēties, izmantojot, Jūsu izvēlēto pakalpojumu sniedzēju.
 
 manage_ssh_keys=Pārvaldīt SSH atslēgas
@@ -726,7 +754,6 @@ gpg_invalid_token_signature=Norādītā GPG atslēga, paraksts un pilnvara neatb
 gpg_token_required=Jānorāda paraksts zemāk esošajai pilnvarai
 gpg_token=Pilnvara
 gpg_token_help=Parakstu ir iespējams uzģenerēt izmantojot komandu:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Tekstuāls GPG paraksts
 key_signature_gpg_placeholder=Sākas ar '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=GPG atslēga "%s" veiksmīgi pārbaudīta.
@@ -776,6 +803,7 @@ ssh_externally_managed=Šim lietotājam SSH atslēga tiek pāvaldīta attālinā
 manage_social=Pārvaldīt piesaistītos sociālos kontus
 social_desc=Šie sociālo tīklu konti var tikt izmantoti, lai pieteiktos. Pārliecinieties, ka visi ir atpazīstami.
 unbind=Atsaistīt
+unbind_success=Sociālā tīkla konts tika veiksmīgi noņemts.
 
 manage_access_token=Pārvaldīt piekļuves pilnvaras
 generate_new_token=Izveidot jaunu pilnvaru
@@ -795,7 +823,9 @@ permissions_public_only=Tikai publiskie
 permissions_access_all=Visi (publiskie, privātie un ierobežotie)
 select_permissions=Norādiet tiesības
 permission_no_access=Nav piekļuves
-permission_read=Izlasītie
+permission_read=Skatīšanās
+permission_write=Skatīšanās un raksīšanas
+access_token_desc=Atzīmētie pilnvaras apgabali ierobežo autentifikāciju tikai atbilstošiem <a %s>API</a> izsaukumiem. Sīkāka informācija pieejama <a %s>dokumentācijā</a>.
 at_least_one_permission=Nepieciešams norādīt vismaz vienu tiesību, lai izveidotu pilnvaru
 permissions_list=Tiesības:
 
@@ -807,6 +837,8 @@ remove_oauth2_application_desc=Noņemot OAuth2 lietotni, tiks noņemta piekļuve
 remove_oauth2_application_success=Lietotne tika dzēsta.
 create_oauth2_application=Izveidot jaunu OAuth2 lietotni
 create_oauth2_application_button=Izveidot lietotni
+create_oauth2_application_success=Ir veiksmīgi izveidota jauna OAuth2 lietotne.
+update_oauth2_application_success=Ir veiksmīgi atjaunota OAuth2 lietotne.
 oauth2_application_name=Lietotnes nosaukums
 oauth2_confidential_client=Konfidenciāls klients. Norādiet lietotēm, kas glabā noslēpumu slepenībā, piemēram, tīmekļa lietotnēm. Nenorādiet instalējamām lietotnēm, tai skaitā darbavirsmas vai mobilajām lietotnēm.
 oauth2_redirect_uris=Pārsūtīšanas URI. Norādiet katru URI savā rindā.
@@ -815,19 +847,26 @@ oauth2_client_id=Klienta ID
 oauth2_client_secret=Klienta noslēpums
 oauth2_regenerate_secret=Pārģenerēt noslēpumus
 oauth2_regenerate_secret_hint=Pazaudēts noslēpums?
+oauth2_client_secret_hint=Pēc šīs lapas pamešanas vai atsvaidzināšanas noslēpums vairs netiks parādīts. Lūgums pārliecināties, ka tas ir saglabāts.
 oauth2_application_edit=Labot
 oauth2_application_create_description=OAuth2 lietotnes ļauj trešas puses lietotnēm piekļūt lietotāja kontiem šajā instancē.
+oauth2_application_remove_description=OAuth2 lietotnes noņemšana liegs tai piekļūt pilnvarotiem lietotāju kontiem šajā instancē. Vai turpināt?
+oauth2_application_locked=Gitea sāknēšanas brīdī reģistrē dažas OAuth2 lietotnes, ja tas ir iespējots konfigurācijā. Lai novērstu negaidītu uzvedību, tās nevar ne labot, ne noņemt. Lūgums vērsties OAuth2 dokumentācijā pēc vairāk informācijas.
 
 authorized_oauth2_applications=Autorizētās OAuth2 lietotnes
+authorized_oauth2_applications_description=Ir ļauta piekļuve savam Gitea kontam šīm trešo pušu lietotnēm. Lūgums atsaukt piekļuvi lietotnēm, kas vairs nav nepieciešamas.
 revoke_key=Atsaukt
 revoke_oauth2_grant=Atsaukt piekļuvi
 revoke_oauth2_grant_description=Atsaucot piekļuvi šai trešas puses lietotnei tiks liegta piekļuve Jūsu datiem. Vai turpināt?
+revoke_oauth2_grant_success=Piekļuve veiksmīgi atsaukta.
 
 twofa_desc=Divfaktoru autentifikācija uzlabo konta drošību.
+twofa_recovery_tip=Ja ierīce tiek pazaudēta, iespējams izmantot vienreiz izmantojamo atkopšanas atslēgu, lai atgūtu piekļuvi savam kontam.
 twofa_is_enrolled=Kontam ir <strong>ieslēgta</strong> divfaktoru autentifikācija.
 twofa_not_enrolled=Kontam šobrīd nav ieslēgta divfaktoru autentifikācija.
 twofa_disable=Atslēgt divfaktoru autentifikāciju
 twofa_scratch_token_regenerate=Ģenerēt jaunu vienreizējo kodu
+twofa_scratch_token_regenerated=Vienreizējā pilnvara tagad ir %s. Tā ir jāglabā drošā vietā, tā vairs nekad netiks rādīta.
 twofa_enroll=Ieslēgt divfaktoru autentifikāciju
 twofa_disable_note=Nepieciešamības gadījumā divfaktoru autentifikāciju ir iespējams atslēgt.
 twofa_disable_desc=Atslēdzot divfaktoru autentifikāciju, konts vairs nebūs tik drošs. Vai turpināt?
@@ -845,6 +884,8 @@ webauthn_register_key=Pievienot drošības atslēgu
 webauthn_nickname=Segvārds
 webauthn_delete_key=Noņemt drošības atslēgu
 webauthn_delete_key_desc=Noņemot drošības atslēgu ar to vairs nebūs iespējams pieteikties. Vai turpināt?
+webauthn_key_loss_warning=Ja tiek pazaudētas drošības atslēgas, tiks zaudēta piekļuve kontam.
+webauthn_alternative_tip=Ir vēlams uzstādīt papildu autentifikācijas veidu.
 
 manage_account_links=Pārvaldīt saistītos kontus
 manage_account_links_desc=Šādi ārējie konti ir piesaistīti Jūsu Gitea kontam.
@@ -854,8 +895,10 @@ remove_account_link=Noņemt saistīto kontu
 remove_account_link_desc=Noņemot saistīto kontu, tam tiks liegta piekļuve Jūsu Gitea kontam. Vai turpināt?
 remove_account_link_success=Saistītais konts tika noņemts.
 
+hooks.desc=Pievienot tīmekļa āķus, kas izpildīsies <strong>visos repozitorijos</strong>, kas jums pieder.
 
 orgs_none=Jūs neesat nevienas organizācijas biedrs.
+repos_none=Jums nepieder neviens repozitorijs.
 
 delete_account=Dzēst savu kontu
 delete_prompt=Šī darbība pilnībā izdzēsīs Jūsu kontu, kā arī tā ir <strong>NEATGRIEZENISKA</strong>.
@@ -874,9 +917,12 @@ visibility=Lietotāja redzamība
 visibility.public=Publisks
 visibility.public_tooltip=Redzams ikvienam
 visibility.limited=Ierobežota
+visibility.limited_tooltip=Redzams tikai autentificētiem lietotājiem
 visibility.private=Privāts
+visibility.private_tooltip=Redzams tikai organizāciju, kurām esi pievienojies, dalībniekiem
 
 [repo]
+new_repo_helper=Repozitorijs satur visus projekta failus, tajā skaitā izmaiņu vēsturi. Jau tiek glabāts kaut kur citur? <a href="%s">Pārnest repozitoriju.</a>
 owner=Īpašnieks
 owner_helper=Ņemot vērā maksimālā repozitoriju skaita ierobežojumu, ne visas organizācijas var tikt parādītas sarakstā.
 repo_name=Repozitorija nosaukums
@@ -888,6 +934,7 @@ template_helper=Padarīt repozitoriju par sagatavi
 template_description=Sagatavju repozitoriji tiek izmantoti, lai balstoties uz tiem veidotu jaunus repozitorijus saglabājot direktoriju un failu struktūru.
 visibility=Redzamība
 visibility_description=Tikai organizācijas īpašnieks vai tās biedri, kam ir tiesības, varēs piekļūt šim repozitorijam.
+visibility_helper=Padarīt repozitoriju privātu
 visibility_helper_forced=Jūsu sistēmas administrators ir noteicis, ka visiem no jauna izveidotajiem repozitorijiem ir jābūt privātiem.
 visibility_fork_helper=(Šīs vērtības maiņa ietekmēs arī visus atdalītos repozitorijus.)
 clone_helper=Nepieciešama palīdzība klonēšanā? Apmeklē <a target="_blank" rel="noopener noreferrer" href="%s">palīdzības</a> sadaļu.
@@ -896,8 +943,10 @@ fork_from=Atdalīt no
 already_forked=Repozitorijs %s jau ir atdalīts
 fork_to_different_account=Atdalīt uz citu kontu
 fork_visibility_helper=Atdalītam repozitorijam nav iespējams mainīt tā redzamību.
+fork_branch=Atzars, ko klonēt atdalītajā repozitorijā
+all_branches=Visi atzari
+fork_no_valid_owners=Šim repozitorijam nevar izveidot atdalītu repozitoriju, jo tam nav spēkā esošu īpašnieku.
 use_template=Izmantot šo sagatavi
-clone_in_vsc=Atvērt VS Code
 download_zip=Lejupielādēt ZIP
 download_tar=Lejupielādēt TAR.GZ
 download_bundle=Lejupielādēt BUNDLE
@@ -923,7 +972,8 @@ trust_model_helper_committer=Revīzijas iesūtītāja: Uzticēties parakstiem, k
 trust_model_helper_collaborator_committer=Līdzstrādnieka un revīzijas iesūtītāja: Uzticēties līdzstrādnieku parakstiem, kas atbilst revīzijas iesūtītājam
 trust_model_helper_default=Noklusētais: Izmantojiet šī servera noklusēto uzticamības modeli
 create_repo=Izveidot repozitoriju
-default_branch=Noklusējuma atzars
+default_branch=Noklusētais atzars
+default_branch_label=noklusējuma
 default_branch_helper=Noklusētais atzars nosaka pamata atzaru uz kuru tiks veidoti izmaiņu pieprasījumi un koda revīziju iesūtīšana.
 mirror_prune=Izmest
 mirror_prune_desc=Izdzēst visas ārējās atsauces, kas ārējā repozitorijā vairs neeksistē
@@ -932,6 +982,8 @@ mirror_interval_invalid=Nekorekts spoguļošanas intervāls.
 mirror_sync_on_commit=Sinhronizēt, kad revīzijas tiek iesūtītas
 mirror_address=Spoguļa adrese
 mirror_address_desc=Pieslēgšanās rekvizītus norādiet autorizācijas sadaļā.
+mirror_address_url_invalid=Norādītais URL ir nederīgs. Visas URL daļas ir jānorāda pareizi.
+mirror_address_protocol_invalid=Norādītais URL ir nederīgs. Var spoguļot tikai no http(s):// vai git:// adresēm.
 mirror_lfs=Lielu failu glabātuve (LFS)
 mirror_lfs_desc=Aktivizēt LFS datu spoguļošanu.
 mirror_lfs_endpoint=LFS galapunkts
@@ -942,7 +994,7 @@ mirror_password_blank_placeholder=(nav uzstādīts)
 mirror_password_help=Nomainiet lietotāju, lai izdzēstu saglabāto paroli.
 watchers=Novērotāji
 stargazers=Zvaigžņdevēji
-stars_remove_warning=Tiks noņemtas visas atzīmētās zvaigznes šim repozitorijam.
+stars_remove_warning=Šis repozitorijs tiks noņemts no visām izlasēm.
 forks=Atdalītie repozitoriji
 reactions_more=un vēl %d
 unit_disabled=Administrators ir atspējojies šo repozitorija sadaļu.
@@ -957,19 +1009,27 @@ delete_preexisting=Dzēst jau eksistējošos failus
 delete_preexisting_content=Dzēst failus direktorijā %s
 delete_preexisting_success=Dzēst nepārņemtos failus direktorijā %s
 blame_prior=Aplūkot vainīgo par izmaiņām pirms šīs revīzijas
+blame.ignore_revs=Neņem vērā izmaiņas no <a href="%s">.git-blame-ignore-revs</a>. Nospiediet <a href="%s">šeit, lai to apietu</a> un redzētu visu izmaiņu skatu.
+blame.ignore_revs.failed=Neizdevās neņemt vērā izmaiņas no <a href="%s">.git-blam-ignore-revs</a>.
 author_search_tooltip=Tiks attēloti ne vairāk kā 30 lietotāji
 
+tree_path_not_found_commit=Revīzijā %[2]s neeksistē ceļš %[1]s
+tree_path_not_found_branch=Atzarā %[2]s nepastāv ceļš %[1]s
+tree_path_not_found_tag=Tagā %[2]s nepastāv ceļš %[1]s
 
 transfer.accept=Apstiprināt īpašnieka maiņu
 transfer.accept_desc=`Mainīt īpašnieku uz "%s"`
 transfer.reject=Noraidīt īpašnieka maiņu
 transfer.reject_desc=`Atcelt īpašnieka maiņu uz "%s"`
+transfer.no_permission_to_accept=Nav atļaujas pieņemt šo pārsūtīšanu.
+transfer.no_permission_to_reject=Nav atļaujas noraidīt šo pārsūtīšanu.
 
 desc.private=Privāts
 desc.public=Publisks
 desc.template=Sagatave
 desc.internal=Iekšējs
 desc.archived=Arhivēts
+desc.sha256=SHA256
 
 template.items=Sagataves ieraksti
 template.git_content=Git saturs (noklusētais atzars)
@@ -982,6 +1042,8 @@ template.issue_labels=Problēmu etiķetes
 template.one_item=Norādiet vismaz vienu sagataves vienību
 template.invalid=Norādiet sagataves repozitoriju
 
+archive.title=Šis repozitorijs ir arhivēts. Ir iespējams aplūkot tā failus un to konēt, bet nav iespējams iesūtīt izmaiņas, kā arī izveidot jaunas problēmas vai izmaiņu pieprasījumus.
+archive.title_date=Šis repozitorijs tika arhivēts %s. Ir iespējams aplūkot tā failus un to konēt, bet nav iespējams iesūtīt izmaiņas, kā arī izveidot jaunas problēmas vai izmaiņu pieprasījumus.
 archive.issue.nocomment=Repozitorijs ir arhivēts. Problēmām nevar pievienot jaunus komentārus.
 archive.pull.nocomment=Repozitorijs ir arhivēts. Izmaiņu pieprasījumiem nevar pievienot jaunus komentārus.
 
@@ -998,6 +1060,7 @@ migrate_options_lfs=Migrēt LFS failus
 migrate_options_lfs_endpoint.label=LFS galapunkts
 migrate_options_lfs_endpoint.description=Migrācija mēģinās izmantot attālināto URL, lai <a target="_blank" rel="noopener noreferrer" href="%s">noteiktu LFS serveri</a>. Var norādīt arī citu galapunktu, ja repozitorija LFS dati ir izvietoti citā vietā.
 migrate_options_lfs_endpoint.description.local=Iespējams norādīt arī servera ceļu.
+migrate_options_lfs_endpoint.placeholder=Ja nav norādīts, galamērķis tiks atvasināts no klonēšanas URL
 migrate_items=Vienības, ko pārņemt
 migrate_items_wiki=Vikivietni
 migrate_items_milestones=Atskaites punktus
@@ -1048,11 +1111,11 @@ generated_from=ģenerēts no
 fork_from_self=Nav iespējams atdalīt repozitoriju, kuram esat īpašnieks.
 fork_guest_user=Piesakieties, lai atdalītu repozitoriju.
 watch_guest_user=Piesakieties, lai sekotu šim repozitorijam.
-star_guest_user=Piesakieties, lai atzīmētu šo repozitoriju ar zvaigznīti.
+star_guest_user=Piesakieties, lai pievienotu šo repozitoriju izlasei.
 unwatch=Nevērot
 watch=Vērot
 unstar=Noņemt zvaigznīti
-star=Pievienot zvaigznīti
+star=Pievienot izlasei
 fork=Atdalīts
 download_archive=Lejupielādēt repozitoriju
 more_operations=Vairāk darbību
@@ -1100,6 +1163,10 @@ file_view_rendered=Skatīt rezultātu
 file_view_raw=Rādīt neapstrādātu
 file_permalink=Patstāvīgā saite
 file_too_large=Šis fails ir par lielu, lai to parādītu.
+invisible_runes_header=`Šīs fails satur neredzamus unikoda simbolus`
+invisible_runes_description=`Šis fails satur neredzamus unikoda simbolus, kas ir neatšķirami cilvēkiem, bet dators tās var atstrādāt atšķirīgi. Ja šķiet, ka tas ir ar nolūku, šo brīdinājumu var droši neņemt vērā. Jāizmanto atsoļa taustiņš (Esc), lai atklātu tās.`
+ambiguous_runes_header=`Šis fails satur neviennozīmīgus unikoda simbolus`
+ambiguous_runes_description=`Šis fails satur unikoda simbolus, kas var tikt sajauktas ar citām rakstzīmēm. Ja šķiet, ka tas ir ar nolūku, šo brīdinājumu var droši neņemt vērā. Jāizmanto atsoļa taustiņš (Esc), lai atklātu tās.`
 invisible_runes_line=`Šī līnija satur neredzamus unikoda simbolus`
 ambiguous_runes_line=`Šī līnija satur neviennozīmīgus unikoda simbolus`
 ambiguous_character=`%[1]c [U+%04[1]X] var tikt sajaukts ar %[2]c [U+%04[2]X]`
@@ -1112,11 +1179,15 @@ video_not_supported_in_browser=Jūsu pārlūks neatbalsta HTML5 video.
 audio_not_supported_in_browser=Jūsu pārlūks neatbalsta HTML5 audio.
 stored_lfs=Saglabāts Git LFS
 symbolic_link=Simboliska saite
+executable_file=Izpildāmais fails
 commit_graph=Revīziju grafs
 commit_graph.select=Izvēlieties atzarus
 commit_graph.hide_pr_refs=Paslēpt izmaiņu pieprasījumus
 commit_graph.monochrome=Melnbalts
 commit_graph.color=Krāsa
+commit.contained_in=Šī revīzija ir iekļauta:
+commit.contained_in_default_branch=Šī revīzija ir daļa no noklusētā atzara
+commit.load_referencing_branches_and_tags=Ielādēt atzarus un tagus, kas atsaucas uz šo revīziju
 blame=Vainot
 download_file=Lejupielādēt failu
 normal_view=Parastais skats
@@ -1195,9 +1266,7 @@ commits.desc=Pārlūkot pirmkoda izmaiņu vēsturi.
 commits.commits=Revīzijas
 commits.no_commits=Nav kopīgu revīziju. Atzariem "%s" un "%s" ir pilnībā atšķirīga izmaiņu vēsture.
 commits.nothing_to_compare=Atzari ir vienādi.
-commits.search=Meklēt revīzijas…
 commits.search.tooltip=Jūs varat izmantot atslēgas vārdus "author:", "committer:", "after:" vai "before:", piemēram, "revert author:Alice before:2019-01-13".
-commits.find=Meklēt
 commits.search_all=Visi atzari
 commits.author=Autors
 commits.message=Ziņojums
@@ -1209,6 +1278,7 @@ commits.signed_by_untrusted_user=Parakstījis neuzticams lietotājs
 commits.signed_by_untrusted_user_unmatched=Parakstījis neuzticams lietotājs, kas neatbilst izmaiņu autoram
 commits.gpg_key_id=GPG atslēgas ID
 commits.ssh_key_fingerprint=SSH atslēgas identificējošā zīmju virkne
+commits.view_path=Skatīt šajā vēstures punktā
 
 commit.operations=Darbības
 commit.revert=Atgriezt
@@ -1219,7 +1289,7 @@ commit.cherry-pick-header=Izlasīt: %s
 commit.cherry-pick-content=Norādiet atzaru uz kuru izlasīt:
 
 commitstatus.error=Kļūda
-commitstatus.failure=Neveiksmīgs
+commitstatus.failure=Kļūme
 commitstatus.pending=Nav iesūtīts
 commitstatus.success=Pabeigts
 
@@ -1247,7 +1317,6 @@ projects.type.basic_kanban=`Vienkāršots "Kanban"`
 projects.type.bug_triage=Kļūdu šķirošana
 projects.template.desc=Projekta sagatave
 projects.template.desc_helper=Izvēlieties projekta sagatavi, lai sāktu darbu
-projects.type.uncategorized=Bez kategorijas
 projects.column.edit=Rediģēt kolonnas
 projects.column.edit_title=Nosaukums
 projects.column.new_title=Nosaukums
@@ -1255,10 +1324,7 @@ projects.column.new_submit=Izveidot kolonnu
 projects.column.new=Jauna kolonna
 projects.column.set_default=Izvēlēties kā noklusēto
 projects.column.set_default_desc=Izvēlēties šo kolonnu kā noklusēto nekategorizētām problēmām un izmaiņu pieteikumiem
-projects.column.unset_default=Atiestatīt noklusēto
-projects.column.unset_default_desc=Noņemt šo kolonnu kā noklusēto
 projects.column.delete=Dzēst kolonnu
-projects.column.deletion_desc=Dzēšot projekta kolonnu visas tam piesaistītās problēmas tiks pārliktas kā nekategorizētas. Vai turpināt?
 projects.column.color=Krāsa
 projects.open=Aktīvie
 projects.close=Pabeigtie
@@ -1336,14 +1402,15 @@ issues.delete_branch_at=`izdzēsa atzaru <b>%s</b> %s`
 issues.filter_label=Etiķete
 issues.filter_label_exclude=`Izmantojiet <code>alt</code> + <code>peles klikšķis vai enter</code>, lai neiekļautu etiķeti`
 issues.filter_label_no_select=Visas etiķetes
+issues.filter_label_select_no_label=Nav etiķetes
 issues.filter_milestone=Atskaites punkts
 issues.filter_milestone_all=Visi atskaites punkti
 issues.filter_milestone_none=Nav atskaites punkta
 issues.filter_milestone_open=Atvērtie atskaites punkti
 issues.filter_milestone_closed=Aizvērtie atskaites punkti
-issues.filter_project=Projektus
+issues.filter_project=Projekts
 issues.filter_project_all=Visi projekti
-issues.filter_project_none=Nav projektu
+issues.filter_project_none=Nav projekta
 issues.filter_assignee=Atbildīgais
 issues.filter_assginee_no_select=Visi atbildīgie
 issues.filter_assginee_no_assignee=Nav atbildīgā
@@ -1389,6 +1456,7 @@ issues.next=Nākamā
 issues.open_title=Atvērta
 issues.closed_title=Slēgta
 issues.draft_title=Melnraksts
+issues.num_comments_1=%d komentārs
 issues.num_comments=%d komentāri
 issues.commented_at=`komentēja <a href="#%s">%s</a>`
 issues.delete_comment_confirm=Vai patiešām vēlaties dzēst šo komentāru?
@@ -1397,6 +1465,7 @@ issues.context.quote_reply=Atbildēt citējot
 issues.context.reference_issue=Atsaukties uz šo jaunā problēmā
 issues.context.edit=Labot
 issues.context.delete=Dzēst
+issues.no_content=Nav sniegts apraksts.
 issues.close=Slēgt problēmu
 issues.comment_pull_merged_at=saplidināta revīzija %[1]s atzarā %[2]s %[3]s
 issues.comment_manually_pull_merged_at=manuāli saplidināta revīzija %[1]s atzarā %[2]s %[3]s
@@ -1415,8 +1484,17 @@ issues.ref_closed_from=`<a href="%[3]s">aizvēra problēmu %[4]s</a> <a id="%[1]
 issues.ref_reopened_from=`<a href="%[3]s">atkārtoti atvēra problēmu %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_from=`no %[1]s`
 issues.author=Autors
+issues.author_helper=Šis lietotājs ir autors.
 issues.role.owner=Īpašnieks
-issues.role.member=Biedri
+issues.role.owner_helper=Šis lietotājs ir šī repozitorija īpašnieks.
+issues.role.member=Dalībnieks
+issues.role.member_helper=Šis lietotājs ir organizācijas, kurai pieder šis repozitorijs, dalībnieks.
+issues.role.collaborator=Līdzstrādnieks
+issues.role.collaborator_helper=Šis lietotājs ir uzaicināts līdzdarboties repozitorijā.
+issues.role.first_time_contributor=Pirmreizējs līdzradītājs
+issues.role.first_time_contributor_helper=Šis ir pirmais šī lietotāja ieguldījums šājā repozitorijā.
+issues.role.contributor=Līdzradītājs
+issues.role.contributor_helper=Šis lietotājs repozitorijā ir iepriekš veicis labojumus.
 issues.re_request_review=Pieprasīt atkārtotu recenziju
 issues.is_stale=Šajā izmaiņu pieprasījumā ir notikušas izmaiņās, kopš veicāt tā recenziju
 issues.remove_request_review=Noņemt recenzijas pieprasījumu
@@ -1431,6 +1509,9 @@ issues.label_title=Etiķetes nosaukums
 issues.label_description=Etiķetes apraksts
 issues.label_color=Etiķetes krāsa
 issues.label_exclusive=Ekskluzīvs
+issues.label_archive=Arhīvēt etiķeti
+issues.label_archived_filter=Rādīt arhivētās etiķetes
+issues.label_archive_tooltip=Arhivētās etiķetes pēc noklusējuma netiek iekļautas ieteikumos, kad meklē pēc nosaukuma.
 issues.label_exclusive_desc=Nosauciet etiķeti <code>grupa/nosaukums</code>, lai grupētu etiķētes un varētu norādīt tās kā ekskluzīvas ar citām <code>grupa/</code> etiķetēm.
 issues.label_exclusive_warning=Jebkura konfliktējoša ekskluzīvas grupas etiķete tiks noņemta, labojot pieteikumu vai izmaiņu pietikumu etiķetes.
 issues.label_count=%d etiķetes
@@ -1485,6 +1566,7 @@ issues.tracking_already_started=`Jau ir uzsākta laika uzskaite par <a href="%s"
 issues.stop_tracking=Apturēt taimeri
 issues.stop_tracking_history=` beidza strādāt %s`
 issues.cancel_tracking=Atmest
+issues.cancel_tracking_history=`atcēla laika uzskaiti %s`
 issues.add_time=Manuāli pievienot laiku
 issues.del_time=Dzēst šo laika žurnāla ierakstu
 issues.add_time_short=Pievienot laiku
@@ -1508,6 +1590,7 @@ issues.due_date_form=dd.mm.yyyy
 issues.due_date_form_add=Pievienot izpildes termiņu
 issues.due_date_form_edit=Labot
 issues.due_date_form_remove=Noņemt
+issues.due_date_not_writer=Ir nepieciešama rakstīšanas piekļuve šim repozitorijam, lai varētu mainīt problēmas plānoto izpildes datumu.
 issues.due_date_not_set=Izpildes termiņš nav uzstādīts.
 issues.due_date_added=pievienoja izpildes termiņu %s %s
 issues.due_date_modified=mainīja termiņa datumu no %[2]s uz %[1]s %[3]s
@@ -1563,6 +1646,9 @@ issues.review.pending.tooltip=Šis komentārs nav redzams citiem lietotājiem. L
 issues.review.review=Recenzija
 issues.review.reviewers=Recenzenti
 issues.review.outdated=Novecojis
+issues.review.outdated_description=Saturs ir mainījies kopš šī komentāra pievienošanas
+issues.review.option.show_outdated_comments=Rādīt novecojušus komentārus
+issues.review.option.hide_outdated_comments=Paslēpt novecojušus komentārus
 issues.review.show_outdated=Rādīt novecojušu
 issues.review.hide_outdated=Paslēpt novecojušu
 issues.review.show_resolved=Rādīt atrisināto
@@ -1601,7 +1687,13 @@ pulls.compare_compare=salīdzināmais
 pulls.switch_comparison_type=Mainīt salīdzināšanas tipu
 pulls.switch_head_and_base=Mainīt galvas un pamata atzarus
 pulls.filter_branch=Filtrēt atzarus
-pulls.no_results=Nekas netika atrasts.
+pulls.show_all_commits=Rādīt visas revīzijas
+pulls.show_changes_since_your_last_review=Rādīt izmaiņas kopš Tavas pēdējās recenzijas
+pulls.showing_only_single_commit=Rāda tikai revīzijas %[1]s izmaiņas
+pulls.showing_specified_commit_range=Rāda tikai izmaiņas starp %[1]s..%[2]s
+pulls.select_commit_hold_shift_for_range=Atlasīt revīziju. Jātur Shift + klikšķis, lai atlasītu vairākas
+pulls.review_only_possible_for_full_diff=Recenzēšana ir iespējama tikai tad, kad tiek apskatīts pilns salīdzinājums
+pulls.filter_changes_by_commit=Atlasīt pēc revīzijas
 pulls.nothing_to_compare=Nav ko salīdzināt, jo bāzes un salīdzināmie atzari ir vienādi.
 pulls.nothing_to_compare_and_allow_empty_pr=Šie atzari ir vienādi. Izveidotais izmaiņu pieprasījums būs tukšs.
 pulls.has_pull_request=`Izmaiņu pieprasījums starp šiem atzariem jau eksistē: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1633,6 +1725,12 @@ pulls.is_empty=Mērķa atzars jau satur šī atzara izmaiņas. Šī revīzija b
 pulls.required_status_check_failed=Dažas no pārbaudēm nebija veiksmīgas.
 pulls.required_status_check_missing=Trūkst dažu obligāto pārbaužu.
 pulls.required_status_check_administrator=Kā administrators Jūs varat sapludināt šo izmaiņu pieprasījumu.
+pulls.blocked_by_approvals=Šim izmaiņu pieprasījumam vēl nav pietiekami daudz apstiprinājumu. Nodrošināti %d no %d apstiprinājumiem.
+pulls.blocked_by_rejection=Šim izmaiņu pieprasījumam oficiālais recenzents ir pieprasījis labojumus.
+pulls.blocked_by_official_review_requests=Šim izmaiņu pieprasījumam ir oficiāli recenzijas pieprasījumi.
+pulls.blocked_by_outdated_branch=Šis izmaiņu pieprasījums ir bloķēts, jo tas ir novecojis.
+pulls.blocked_by_changed_protected_files_1=Šis izmaiņu pieprasījums ir bloķēts, jo tas izmaina aizsargāto failu:
+pulls.blocked_by_changed_protected_files_n=Šis izmaiņu pieprasījums ir bloķēts, jo tas izmaina aizsargātos failus:
 pulls.can_auto_merge_desc=Šo izmaiņu pieprasījumu var automātiski sapludināt.
 pulls.cannot_auto_merge_desc=Šis izmaiņu pieprasījums nevar tikt automātiski sapludināts konfliktu dēļ.
 pulls.cannot_auto_merge_helper=Sapludiniet manuāli, lai atrisinātu konfliktus.
@@ -1667,6 +1765,7 @@ pulls.rebase_conflict_summary=Kļūdas paziņojums
 pulls.unrelated_histories=Sapludināšana neizdevās: mērķa un bāzes atzariem nav kopējas vēstures. Ieteikums: izvēlieties citu sapludināšanas stratēģiju
 pulls.merge_out_of_date=Sapludināšana neizdevās: sapludināšanas laikā, bāzes atzarā tika iesūtītas izmaiņas. Ieteikums: mēģiniet atkārtoti.
 pulls.head_out_of_date=Sapludināšana neizdevās: sapludināšanas laikā, bāzes atzarā tika iesūtītas izmaiņas. Ieteikums: mēģiniet atkārtoti.
+pulls.has_merged=Neizdevās: izmaiņu pieprasījums jau ir sapludināts, nevar to darīt atkārtoti vai mainīt mērķa atzaru.
 pulls.push_rejected=Sapludināšana neizdevās: iesūtīšana tika noraidīta. Pārbaudiet git āķus šim repozitorijam.
 pulls.push_rejected_summary=Pilns noraidīšanas ziņojums
 pulls.push_rejected_no_message=Sapludināšana neizdevās: Izmaiņu iesūtīšana tika noraidīta, bet serveris neatgrieza paziņojumu.<br>Pārbaudiet git āķus šim repozitorijam
@@ -1678,6 +1777,8 @@ pulls.status_checks_failure=Dažas pārbaudes neizdevās izpildīt
 pulls.status_checks_error=Dažu pārbaužu izpildes laikā, radās kļūdas
 pulls.status_checks_requested=Obligāts
 pulls.status_checks_details=Papildu informācija
+pulls.status_checks_hide_all=Paslēpt visas pārbaudes
+pulls.status_checks_show_all=Parādīt visas pārbaudes
 pulls.update_branch=Atjaunot atzaru, izmantojot, sapludināšanu
 pulls.update_branch_rebase=Atjaunot atzaru, izmantojot, pārbāzēšanu
 pulls.update_branch_success=Atzara atjaunināšana veiksmīgi pabeigta
@@ -1686,6 +1787,11 @@ pulls.outdated_with_base_branch=Atzars ir novecojis salīdzinot ar bāzes atzaru
 pulls.close=Aizvērt izmaiņu pieprasījumu
 pulls.closed_at=`aizvēra šo izmaiņu pieprasījumu <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 pulls.reopened_at=`atkārtoti atvēra šo izmaiņu pieprasījumu <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.cmd_instruction_hint=`Apskatīt <a class="show-instruction">komandrindas izmantošanas norādes</a>.`
+pulls.cmd_instruction_checkout_title=Paņemt
+pulls.cmd_instruction_checkout_desc=Projekta repozitorijā jāizveido jauns atzars un jāpārbauda izmaiņas.
+pulls.cmd_instruction_merge_title=Sapludināt
+pulls.cmd_instruction_merge_desc=Sapludināt izmaiņas un atjaunot tās Gitea.
 pulls.clear_merge_message=Notīrīt sapludināšanas ziņojumu
 pulls.clear_merge_message_hint=Notīrot sapludināšanas ziņojumu tiks noņemts tikai pats ziņojums, bet tiks paturēti ģenerētie git ziņojumu, kā "Co-Authored-By …".
 
@@ -1704,7 +1810,9 @@ pulls.auto_merge_canceled_schedule_comment=`atcēla automātisko sapludināšanu
 pulls.delete.title=Dzēst šo izmaiņu pieprasījumu?
 pulls.delete.text=Vai patiešām vēlaties dzēst šo izmaiņu pieprasījumu? (Neatgriezeniski tiks izdzēsts viss saturs. Apsveriet iespēju to aizvērt, ja vēlaties informāciju saglabāt vēsturei)
 
+pulls.recently_pushed_new_branches=Tu iesūtīji izmaiņas atzarā <strong>%[1]s</strong> %[2]s
 
+pull.deleted_branch=(izdzēsts):%s
 
 milestones.new=Jauns atskaites punkts
 milestones.closed=Aizvērts %s
@@ -1712,6 +1820,7 @@ milestones.update_ago=Atjaunots %s
 milestones.no_due_date=Bez termiņa
 milestones.open=Atvērta
 milestones.close=Aizvērt
+milestones.new_subheader=Atskaites punkti var palīdzēt pārvaldīt problēmas un sekot to virzībai.
 milestones.completeness=%d%% pabeigti
 milestones.create=Izveidot atskaites punktu
 milestones.title=Virsraksts
@@ -1728,12 +1837,25 @@ milestones.edit_success=Izmaiņas atskaites punktā "%s" tika veiksmīgi saglab
 milestones.deletion=Dzēst atskaites punktu
 milestones.deletion_desc=Dzēšot šo atskaites punktu, tas tiks noņemts no visām saistītajām problēmām un izmaiņu pieprasījumiem. Vai turpināt?
 milestones.deletion_success=Atskaites punkts tika veiksmīgi izdzēsts.
+milestones.filter_sort.earliest_due_data=Agrākais izpildes laiks
+milestones.filter_sort.latest_due_date=Vēlākais izpildes laiks
 milestones.filter_sort.least_complete=Vismazāk pabeigtais
 milestones.filter_sort.most_complete=Visvairāk pabeigtais
 milestones.filter_sort.most_issues=Visvairāk problēmu
 milestones.filter_sort.least_issues=Vismazāk problēmu
 
+signing.will_sign=Šī revīzija tiks parakstīta ar atslēgu "%s".
 signing.wont_sign.error=Notika kļūda pārbaudot vai revīzija var tikt parakstīta.
+signing.wont_sign.nokey=Nav pieejamas atslēgas, ar ko parakstīt šo revīziju.
+signing.wont_sign.never=Revīzijas nekad netiek parakstītas.
+signing.wont_sign.always=Revīzijas vienmēr tiek parakstītas.
+signing.wont_sign.pubkey=Revīzija netiks parakstīta, jo kontam nav piesaistīta publiskā atslēga.
+signing.wont_sign.twofa=Jābūt iespējotai divfaktoru autentifikācijai, lai parakstītu revīzijas.
+signing.wont_sign.parentsigned=Revīzija netiks parakstīta, jo nav parakstīta vecāka revīzija.
+signing.wont_sign.basesigned=Sapludināšanas revīzija netiks parakstīta, jo pamata revīzija nav parakstīta.
+signing.wont_sign.headsigned=Sapludināšanas revīzija netiks parakstīta, jo galvenā revīzija nav parakstīta.
+signing.wont_sign.commitssigned=Sapludināšana netiks parakstīta, jo visas saistītās revīzijas nav parakstītas.
+signing.wont_sign.approved=Sapludināšana netiks parakstīta, jo izmaiņu pieprasījums nav apstiprināts.
 signing.wont_sign.not_signed_in=Jūs neesat pieteicies.
 
 ext_wiki=Piekļuve ārējai vikivietnei
@@ -1832,16 +1954,7 @@ activity.git_stats_and_deletions=un
 activity.git_stats_deletion_1=%d dzēšana
 activity.git_stats_deletion_n=%d dzēšanas
 
-search=Meklēt
-search.search_repo=Meklēšana repozitorijā
-search.type.tooltip=Meklēšanas veids
-search.fuzzy=Aptuveni
-search.fuzzy.tooltip=Iekļaut meklēšanas rezultātos arī aptuvenas sakritības
-search.match=Precīzi
-search.match.tooltip=Iekļaut meklēšanas rezultātos tikai precīzas sakritības
-search.results=Meklēšanas rezultāti nosacījumam "%s" repozitorijā <a href="%s">%s</a>
-search.code_no_results=Netika atrasts pirmkods, kas atbilstu kritērijiem.
-search.code_search_unavailable=Pašlaik koda meklēšana nav pieejama. Sazinieties ar lapas administratoru.
+contributors.contribution_type.commits=Revīzijas
 
 settings=Iestatījumi
 settings.desc=Iestatījumi ir vieta, kur varat pārvaldīt repozitorija iestatījumus
@@ -1864,6 +1977,7 @@ settings.mirror_settings.docs.disabled_push_mirror.info=Iesūtīšanas spoguļus
 settings.mirror_settings.docs.no_new_mirrors=Šis repozitorijs spoguļo izmaiņas uz vai no cita repozitorija. Pašlaik vairāk nav iespējams izveidot jaunus spoguļa repozitorijus.
 settings.mirror_settings.docs.can_still_use=Lai arī nav iespējams mainīt esošos vai izveidot jaunus spoguļa repozitorijus, esošie turpinās strādāt.
 settings.mirror_settings.docs.pull_mirror_instructions=Lai ietatītu atvilkšanas spoguli, sekojiet instrukcijām:
+settings.mirror_settings.docs.more_information_if_disabled=Vairāk par piegādāšanas un saņemšanas spoguļiem var uzzināt šeit:
 settings.mirror_settings.docs.doc_link_title=Kā spoguļot repozitorijus?
 settings.mirror_settings.docs.doc_link_pull_section=dokumentācijas nodaļā "Pulling from a remote repository".
 settings.mirror_settings.docs.pulling_remote_title=Atvilkt no attāla repozitorija
@@ -1875,8 +1989,11 @@ settings.mirror_settings.last_update=Pēdējās izmaiņas
 settings.mirror_settings.push_mirror.none=Nav konfigurēts iesūtīšanas spogulis
 settings.mirror_settings.push_mirror.remote_url=Git attālinātā repozitorija URL
 settings.mirror_settings.push_mirror.add=Pievienot iesūtīšanas spoguli
+settings.mirror_settings.push_mirror.edit_sync_time=Labot spoguļa sinhronizācijas intervālu
 
 settings.sync_mirror=Sinhronizēt tagad
+settings.pull_mirror_sync_in_progress=Pašlaik tiek saņemtas izmaiņas no attālā %s.
+settings.push_mirror_sync_in_progress=Pašlaik tiek piegādātas izmaiņas uz attālo %s.
 settings.site=Mājas lapa
 settings.update_settings=Mainīt iestatījumus
 settings.update_mirror_settings=Atjaunot spoguļa iestatījumus
@@ -1916,6 +2033,7 @@ settings.pulls.default_allow_edits_from_maintainers=Atļaut uzturētājiem labot
 settings.releases_desc=Iespējot repozitorija laidienus
 settings.packages_desc=Iespējot repozitorija pakotņu reģistru
 settings.projects_desc=Iespējot repozitorija projektus
+settings.projects_mode_all=Visi projekti
 settings.actions_desc=Iespējot repozitorija darbības
 settings.admin_settings=Administratora iestatījumi
 settings.admin_enable_health_check=Iespējot veselības pārbaudi (git fsck) šim repozitorijam
@@ -1943,6 +2061,7 @@ settings.transfer.rejected=Repozitorija īpašnieka maiņas pieprasījums tika n
 settings.transfer.success=Repozitorija īpašnieka maiņa veiksmīga.
 settings.transfer_abort=Atcelt īpašnieka maiņu
 settings.transfer_abort_invalid=Nevar atcelt neeksistējoša repozitorija īpašnieka maiņu.
+settings.transfer_abort_success=Repozitorija īpašnieka maiņa uz %s tika veiksmīgi atcelta.
 settings.transfer_desc=Mainīt šī repozitorija īpašnieku uz citu lietotāju vai organizāciju, kurai Jums ir administratora tiesības.
 settings.transfer_form_title=Ievadiet repozitorija nosaukumu, lai apstiprinātu:
 settings.transfer_in_progress=Pašlaik jau tiek veikta repozitorija īpašnieka maiņa. Atceliet iepriekšējo īpašnieka maiņu, ja vēlaties mainīt uz citu.
@@ -1967,12 +2086,12 @@ settings.trust_model.collaboratorcommitter=Līdzstrādnieka un revīzijas iesūt
 settings.trust_model.collaboratorcommitter.long=Līdzstrādnieka un revīzijas iesūtītāja: Uzticēties līdzstrādnieku parakstiem, kas atbilst revīzijas iesūtītājam
 settings.trust_model.collaboratorcommitter.desc=Derīgi līdzstrādnieku paraksti tiks atzīmēti kā "uzticami", ja tie atbilst revīzijas iesūtītājam, citos gadījumos tie tiks atzīmēti kā "neuzticami", ja paraksts atbilst revīzijas iesūtītajam, vai "nesakrītoši", ja neatbilst. Šis nozīmē, ka Gitea būs kā revīzijas iesūtītājs parakstītām revīzijām, kur īstais revīzijas iesūtītājs tiks atīzmēts revīzijas komentāra beigās ar tekstu Co-Authored-By: un Co-Committed-By:. Noklusētajai Gitea atslēgai ir jāatbilst lietotājam datubāzē.
 settings.wiki_delete=Dzēst vikivietnes datus
-settings.wiki_delete_desc=Vikivietnes repozitorija dzēšana ir <strong>NEATGRIEZENISKA</strong>. Vai turpināt?
+settings.wiki_delete_desc=Vikivietnes repozitorija dzēšana ir neatgriezeniska un nav atsaucama.
 settings.wiki_delete_notices_1=- Šī darbība dzēsīs un atspējos repozitorija %s vikivietni.
 settings.confirm_wiki_delete=Dzēst vikivietnes datus
 settings.wiki_deletion_success=Repozitorija vikivietnes dati tika izdzēsti.
 settings.delete=Dzēst šo repozitoriju
-settings.delete_desc=Repozitorija dzēšana ir <strong>NEATGRIEZENISKA</strong>. Vai turpināt?
+settings.delete_desc=Repozitorija dzēšana ir neatgriezeniska un nav atsaucama.
 settings.delete_notices_1=- Šī darbība ir <strong>NEATGRIEZENISKA</strong>.
 settings.delete_notices_2=- Šī darbība neatgriezeniski izdzēsīs visu repozitorijā <strong>%s</strong>, tai skaitā problēmas, komentārus, vikivietni un līdzstrādnieku piesaisti.
 settings.delete_notices_fork_1=- Visi atdalītie repozitoriju pēc dzēšanas kļūs neatkarīgi.
@@ -1989,7 +2108,6 @@ settings.delete_collaborator=Noņemt
 settings.collaborator_deletion=Noņemt līdzstrādnieku
 settings.collaborator_deletion_desc=Noņemot līdzstrādnieku, tam tiks liegta piekļuve šim repozitorijam. Vai turpināt?
 settings.remove_collaborator_success=Līdzstrādnieks tika noņemts.
-settings.search_user_placeholder=Meklēt lietotāju…
 settings.org_not_allowed_to_be_collaborator=Organizācijas nevar tikt pievienotas kā līdzstrādnieki.
 settings.change_team_access_not_allowed=Iespēja mainīt komandu piekļuvi repozitorijam ir organizācijas īpašniekam
 settings.team_not_in_organization=Komanda nav tajā pašā organizācijā kā repozitorijs
@@ -1997,7 +2115,6 @@ settings.teams=Komandas
 settings.add_team=Pievienot komandu
 settings.add_team_duplicate=Komandai jau ir piekļuve šim repozitorijam
 settings.add_team_success=Komandai tagad ir piekļuve šim repozitorijam.
-settings.search_team=Meklēt komandu…
 settings.change_team_permission_tip=Komandas tiesības tiek uzstādītas komandas iestatījumu lapā un nevar tikt individuāli mainītas katram repozitorijam atsevišķi
 settings.delete_team_tip=Komandai ir piekļuve visiem repozitorijiem un tā nevar tikt noņemta individuāli
 settings.remove_team_success=Komandas piekļuve šim repozitorijam ir noņemta.
@@ -2009,12 +2126,14 @@ settings.webhook_deletion_desc=Noņemot tīmekļa āķi, tiks dzēsti visi tā i
 settings.webhook_deletion_success=Tīmekļa āķis tika noņemts.
 settings.webhook.test_delivery=Testa piegāde
 settings.webhook.test_delivery_desc=Veikt viltus push-notikuma piegādi, lai notestētu Jūsu tīmekļa āķa iestatījumus.
+settings.webhook.test_delivery_desc_disabled=Lai pārbaudītu šo tīmekļa āķi ar neīstu notikumu, tas ir jāiespējo.
 settings.webhook.request=Pieprasījums
 settings.webhook.response=Atbilde
 settings.webhook.headers=Galvenes
 settings.webhook.payload=Saturs
 settings.webhook.body=Saturs
 settings.webhook.replay.description=Izpildīt atkārtoti šo tīmekļa āķi.
+settings.webhook.replay.description_disabled=Lai atkārtoti izpildītu šo tīmekļa āķi, tas ir jāiespējo.
 settings.webhook.delivery.success=Notikums tika veiksmīgi pievienots piegādes rindai. Var paiet vairākas sekundes līdz tas parādās piegādes vēsturē.
 settings.githooks_desc=Git āķus apstrādā pats Git. Jūs varat labot atbalstīto āku failus sarakstā zemāk, lai veiktu pielāgotas darbības.
 settings.githook_edit_desc=Ja āķis nav aktīvs, tiks attēlots piemērs kā to izmantot. Atstājot āķa saturu tukšu, tas tiks atspējots.
@@ -2148,9 +2267,7 @@ settings.protect_whitelist_committers=Atļaut iesūtīt izmaiņas norādītajiem
 settings.protect_whitelist_committers_desc=Tikai norādītiem lietotāji vai komandas varēs iesūtīt izmaiņas šajā atzarā (piespiedu izmaiņu iesūtīšanas netiks atļauta).
 settings.protect_whitelist_deploy_keys=Atļaut izvietošanas atslēgām ar rakstīšanas tiesībām nosūtīt izmaiņas.
 settings.protect_whitelist_users=Lietotāji, kas var veikt izmaiņu nosūtīšanu:
-settings.protect_whitelist_search_users=Meklēt lietotājus…
 settings.protect_whitelist_teams=Komandas, kas var veikt izmaiņu nosūtīšanu:
-settings.protect_whitelist_search_teams=Meklēt komandas…
 settings.protect_merge_whitelist_committers=Iespējot sapludināšanas ierobežošanu
 settings.protect_merge_whitelist_committers_desc=Atļaut tikai noteiktiem lietotājiem vai komandām sapludināt izmaiņu pieprasījumus šajā atzarā.
 settings.protect_merge_whitelist_users=Lietotāji, kas var veikt izmaiņu sapludināšanu:
@@ -2174,6 +2291,7 @@ settings.dismiss_stale_approvals_desc=Kad tiek iesūtītas jaunas revīzijas, ka
 settings.require_signed_commits=Pieprasīt parakstītas revīzijas
 settings.require_signed_commits_desc=Noraidīt iesūtītās izmaiņas šim atzaram, ja tās nav parakstītas vai nav iespējams pārbaudīt.
 settings.protect_branch_name_pattern=Aizsargātā zara šablons
+settings.protect_branch_name_pattern_desc=Aizsargāto atzaru nosaukumu šabloni. Šablonu pierakstu skatīt <a href="https://github.com/gobwas/glob">dokumentācijā</a>. Piemēri: main, release/**
 settings.protect_patterns=Šabloni
 settings.protect_protected_file_patterns=Aizsargāto failu šablons (vairākus var norādīt atdalot ar semikolu ';'):
 settings.protect_protected_file_patterns_desc=Aizsargātie faili, ko nevar mainīt, pat ja lietotājam ir tiesības veidot jaunus, labot vai dzēst failus šajā atzarā. Vairākus šablons ir iespējams norādīt atdalot tos ar semikolu (';'). Sīkāka informācija par šabloniem pieejama <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> dokumentācijā. Piemēram, <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
@@ -2210,18 +2328,26 @@ settings.tags.protection.allowed.teams=Atļauts komandām
 settings.tags.protection.allowed.noone=Nevienam
 settings.tags.protection.create=Aizsargāt tagus
 settings.tags.protection.none=Nav uzstādīta tagu aizsargāšana.
+settings.tags.protection.pattern.description=Var izmantot vienkāršu nosaukumu vai glob šablonu, vai regulāro izteiksmi, lai atbilstu vairākiem tagiem. Vairāk ir lasāms <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">aizsargāto tagu šablonu dokumentācijā</a>.
 settings.bot_token=Bota pilnvara
 settings.chat_id=Tērzēšanas ID
+settings.thread_id=Pavediena ID
 settings.matrix.homeserver_url=Mājas servera URL
 settings.matrix.room_id=Istabas ID
 settings.matrix.message_type=Ziņas veids
 settings.archive.button=Arhivēt
 settings.archive.header=Arhivēt repozitoriju
+settings.archive.text=Repozitorija arhivēšana padarīs to tikai lasāmu. Tas nebūs redzams infopanelī. Neviens nevarēs izveidot jaunas revīzijas vai atvērt jaunus problēmu pieteikumus vai izmaiņu pieprasījumus.
 settings.archive.success=Repozitorijs veiksmīgi arhivēts.
 settings.archive.error=Arhivējot repozitoriju radās neparedzēta kļūda. Pārbaudiet kļūdu žurnālu, lai uzzinātu sīkāk.
 settings.archive.error_ismirror=Nav iespējams arhivēt spoguļotus repozitorijus.
 settings.archive.branchsettings_unavailable=Atzaru iestatījumi nav pieejami, ja repozitorijs ir arhivēts.
 settings.archive.tagsettings_unavailable=Tagu iestatījumi nav pieejami, ja repozitorijs ir arhivēts.
+settings.unarchive.button=Atcelt repozitorija arhivēšanu
+settings.unarchive.header=Atcelt šī repozitorija arhivēšanu
+settings.unarchive.text=Repozitorija arhivēšanas atcelšana atjaunos tā spēju saņemt izmaiņas, kā arī jaunus problēmu pieteikumus un izmaiņu pieprasījumus.
+settings.unarchive.success=Repozitorijam veiksmīgi atcelta arhivācija.
+settings.unarchive.error=Repozitorija arhivēšanas atcelšanas laikā atgadījās kļūda. Vairāk ir redzams žurnālā.
 settings.update_avatar_success=Repozitorija attēls tika atjaunināts.
 settings.lfs=LFS
 settings.lfs_filelist=LFS faili, kas saglabāti šajā repozitorijā
@@ -2288,6 +2414,7 @@ diff.show_more=Rādīt vairāk
 diff.load=Ielādēt izmaiņas
 diff.generated=ģenerēts
 diff.vendored=ārējs
+diff.comment.add_line_comment=Pievienot rindas komentāru
 diff.comment.placeholder=Ievadiet komentāru
 diff.comment.markdown_info=Tiek atbalstīta formatēšana ar Markdown.
 diff.comment.add_single_comment=Pievienot vienu komentāru
@@ -2344,6 +2471,7 @@ release.edit_release=Labot laidienu
 release.delete_release=Dzēst laidienu
 release.delete_tag=Dzēst tagu
 release.deletion=Dzēst laidienu
+release.deletion_desc=Laidiena izdzēšana tikai noņem to no Gitea. Tā neietekmēs Git tagu, repozitorija saturu vai vēsturi. Vai turpināt?
 release.deletion_success=Laidiens tika izdzēsts.
 release.deletion_tag_desc=Tiks izdzēsts tags no repozitorija. Repozitorija saturs un vēsture netiks mainīta. Vai turpināt?
 release.deletion_tag_success=Tags tika izdzēsts.
@@ -2363,6 +2491,7 @@ branch.already_exists=Atzars ar nosaukumu "%s" jau eksistē.
 branch.delete_head=Dzēst
 branch.delete=`Dzēst atzaru "%s"`
 branch.delete_html=Dzēst atzaru
+branch.delete_desc=Atzara dzēšana ir neatgriezeniska. Kaut arī izdzēstais zars neilgu laiku var turpināt pastāvēt, pirms tas tiešām tiek noņemts, to vairumā gadījumu NEVAR atsaukt. Vai turpināt?
 branch.deletion_success=Atzars "%s" tika izdzēsts.
 branch.deletion_failed=Neizdevās izdzēst atzaru "%s".
 branch.delete_branch_has_new_commits=Atzars "%s" nevar tik dzēsts, jo pēc sapludināšanas, tam ir pievienotas jaunas revīzijas.
@@ -2386,7 +2515,7 @@ branch.create_new_branch=Izveidot jaunu atzaru no atzara:
 branch.confirm_create_branch=Izveidot atzaru
 branch.warning_rename_default_branch=Tiks pārsaukts noklusētais atzars.
 branch.rename_branch_to=Pārsaukt "%s" uz:
-branch.confirm_rename_branch=Pārsaukt atzaru
+branch.confirm_rename_branch=Pārdēvēt atzaru
 branch.create_branch_operation=Izveidot atzaru
 branch.new_branch=Izveidot jaunu atzaru
 branch.new_branch_from=`Izveidot jaunu atzaru no "%s"`
@@ -2402,6 +2531,7 @@ tag.create_success=Tags "%s" tika izveidots.
 topic.manage_topics=Pārvaldīt tēmas
 topic.done=Gatavs
 topic.count_prompt=Nevar pievienot vairāk kā 25 tēmas
+topic.format_prompt=Tēmai jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un punktus ('.') un var būt līdz 35 rakstzīmēm gara. Burtiem jābūt mazajiem.
 
 find_file.go_to_file=Iet uz failu
 find_file.no_matching=Atbilstošs fails netika atrasts
@@ -2410,6 +2540,12 @@ error.csv.too_large=Nevar attēlot šo failu, jo tas ir pārāk liels.
 error.csv.unexpected=Nevar attēlot šo failu, jo tas satur neparedzētu simbolu %d. līnijas %d. kolonnā.
 error.csv.invalid_field_count=Nevar attēlot šo failu, jo tas satur nepareizu skaitu ar laukiem %d. līnijā.
 
+[graphs]
+component_loading=Ielādē %s...
+component_loading_failed=Nevarēja ielādēt %s
+component_loading_info=Šis var aizņemt kādu brīdi…
+component_failed_to_load=Atgadījās neparedzēta kļūda.
+
 [org]
 org_name_holder=Organizācijas nosaukums
 org_full_name_holder=Organizācijas pilnais nosaukums
@@ -2440,6 +2576,7 @@ form.create_org_not_allowed=Jums nav tiesību veidot jauno organizāciju.
 settings=Iestatījumi
 settings.options=Organizācija
 settings.full_name=Pilns vārds, uzvārds
+settings.email=E-pasta adrese saziņai
 settings.website=Mājas lapa
 settings.location=Atrašanās vieta
 settings.permission=Tiesības
@@ -2453,6 +2590,7 @@ settings.visibility.private_shortname=Privāta
 
 settings.update_settings=Mainīt iestatījumus
 settings.update_setting_success=Organizācijas iestatījumi tika saglabāti.
+settings.change_orgname_prompt=Piezīme: organizācijas nosaukuma maiņa izmainīs arī organizācijas URL un atbrīvos veco nosaukumu.
 settings.change_orgname_redirect_prompt=Vecais vārds pārsūtīs uz jauno, kamēr vien tas nebūs izmantots.
 settings.update_avatar_success=Organizācijas attēls tika saglabāts.
 settings.delete=Dzēst organizāciju
@@ -2472,7 +2610,7 @@ members.private=Slēpts
 members.private_helper=padarīt redzemu
 members.member_role=Dalībnieka loma:
 members.owner=Īpašnieks
-members.member=Biedri
+members.member=Dalībnieks
 members.remove=Noņemt
 members.remove.detail=Noņemt lietotāju %[1]s no organizācijas %[2]s?
 members.leave=Atstāt
@@ -2512,7 +2650,6 @@ teams.write_permission_desc=Šai komandai ir <strong>rakstīšanas</strong> ties
 teams.admin_permission_desc=Šai komandai ir <strong>administratora</strong> tiesības: dalībnieki var lasīt, rakstīt un pievienot citus dalībniekus komandas repozitorijiem.
 teams.create_repo_permission_desc=Papildus šī komanda piešķirt <strong>Veidot repozitorijus</strong> tiesības: komandas biedri var veidot jaunus repozitorijus šajā organizācijā.
 teams.repositories=Komandas repozitoriji
-teams.search_repo_placeholder=Meklēt repozitorijā…
 teams.remove_all_repos_title=Noņemt visus komandas repozitorijus
 teams.remove_all_repos_desc=Šī darbība noņems visus repozitorijus no komandas.
 teams.add_all_repos_title=Pievienot visus repozitorijus
@@ -2528,27 +2665,34 @@ teams.all_repositories_helper=Šai komandai ir piekļuve visiem repozitorijiem.
 teams.all_repositories_read_permission_desc=Šī komanda piešķirt <strong>skatīšanās</strong> tiesības <strong>visiem repozitorijiem</strong>: komandas biedri var skatīties un klonēt visus organizācijas repozitorijus.
 teams.all_repositories_write_permission_desc=Šī komanda piešķirt <strong>labošanas</strong> tiesības <strong>visiem repozitorijiem</strong>: komandas biedri var skatīties un nosūtīt izmaiņas visiem organizācijas repozitorijiem.
 teams.all_repositories_admin_permission_desc=Šī komanda piešķirt <strong>administratora</strong> tiesības <strong>visiem repozitorijiem</strong>: komandas biedri var skatīties, nosūtīt izmaiņas un mainīt iestatījumus visiem organizācijas repozitorijiem.
+teams.invite.title=Tu esi uzaicināts pievienoties organizācijas <strong>%[2]s</strong> komandai <strong>%[1]s</strong>.
 teams.invite.by=Uzaicināja %s
 teams.invite.description=Nospiediet pogu zemāk, lai pievienotos komandai.
 
 [admin]
 dashboard=Infopanelis
+self_check=Pašpārbaude
+identity_access=Identitāte un piekļuve
 users=Lietotāju konti
 organizations=Organizācijas
+assets=Koda aktīvi
 repositories=Repozitoriji
 hooks=Tīmekļa āķi
+integrations=Integrācijas
 authentication=Autentificēšanas avoti
 emails=Lietotāja e-pasts
 config=Konfigurācija
+config_summary=Kopsavilkums
+config_settings=Iestatījumi
 notices=Sistēmas paziņojumi
 monitor=Uzraudzība
 first_page=Pirmā
 last_page=Pēdējā
 total=Kopā: %d
+settings=Administratora iestatījumi
 
 dashboard.new_version_hint=Ir pieejama Gitea versija %s, pašreizējā versija %s. Papildus informācija par jauno versiju ir pieejama <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">mājas lapā</a>.
 dashboard.statistic=Kopsavilkums
-dashboard.operations=Uzturēšanas darbības
 dashboard.system_status=Sistēmas statuss
 dashboard.operation_name=Darbības nosaukums
 dashboard.operation_switch=Pārslēgt
@@ -2557,11 +2701,13 @@ dashboard.clean_unbind_oauth=Notīrīt nepiesaistītos OAuth savienojumus
 dashboard.clean_unbind_oauth_success=Visi nepiesaistītie OAuth savienojumu tika izdzēsti.
 dashboard.task.started=Uzsākts uzdevums: %[1]s
 dashboard.task.process=Uzdevums: %[1]s
+dashboard.task.cancelled=Uzdevums: %[1]s atcelts: %[3]s
 dashboard.task.error=Kļūda uzdevuma izpildē: %[1]s: %[3]s
 dashboard.task.finished=Uzdevums: %[1]s, ko iniciēja %[2]s ir izpildīts
 dashboard.task.unknown=Nezināms uzdevums: %[1]s
 dashboard.cron.started=Uzsākts Cron: %[1]s
 dashboard.cron.process=Cron: %[1]s
+dashboard.cron.cancelled=Cron: %[1]s atcelts: %[3]s
 dashboard.cron.error=Kļūda Cron: %s: %[3]s
 dashboard.cron.finished=Cron: %[1]s pabeigts
 dashboard.delete_inactive_accounts=Dzēst visus neaktivizētos kontus
@@ -2571,6 +2717,7 @@ dashboard.delete_repo_archives.started=Uzdevums visu repozitoriju arhīvu dzēš
 dashboard.delete_missing_repos=Dzēst visus repozitorijus, kam trūkst Git failu
 dashboard.delete_missing_repos.started=Uzdevums visu repozitoriju dzēšanai, kam trūkst git failu, uzsākts.
 dashboard.delete_generated_repository_avatars=Dzēst ģenerētos repozitoriju attēlus
+dashboard.sync_repo_branches=Sinhronizācija ar dabubāzi izlaida atzarus no git datiem
 dashboard.update_mirrors=Atjaunot spoguļus
 dashboard.repo_health_check=Pārbaudīt visu repozitoriju veselību
 dashboard.check_repo_stats=Pārbaudīt visu repozitoriju statistiku
@@ -2585,6 +2732,7 @@ dashboard.reinit_missing_repos=Atkārtoti inicializēt visus pazaudētos Git rep
 dashboard.sync_external_users=Sinhronizēt ārējo lietotāju datus
 dashboard.cleanup_hook_task_table=Iztīrīt tīmekļa āķu vēsturi
 dashboard.cleanup_packages=Notīrīt novecojušās pakotnes
+dashboard.cleanup_actions=Notīrīt darbību izbeigušos žurnālus un artefaktus
 dashboard.server_uptime=Servera darbības laiks
 dashboard.current_goroutine=Izmantotās Gorutīnas
 dashboard.current_memory_usage=Pašreiz izmantotā atmiņa
@@ -2622,6 +2770,9 @@ dashboard.gc_lfs=Veikt atkritumu uzkopšanas darbus LFS meta objektiem
 dashboard.stop_zombie_tasks=Apturēt zombija uzdevumus
 dashboard.stop_endless_tasks=Apturēt nepārtrauktus uzdevumus
 dashboard.cancel_abandoned_jobs=Atcelt pamestus darbus
+dashboard.start_schedule_tasks=Sākt plānotos uzdevumus
+dashboard.sync_branch.started=Sākta atzaru sinhronizācija
+dashboard.rebuild_issue_indexer=Pārbūvēt problēmu indeksu
 
 users.user_manage_panel=Lietotāju kontu pārvaldība
 users.new_account=Izveidot lietotāja kontu
@@ -2630,6 +2781,9 @@ users.full_name=Vārds, uzvārds
 users.activated=Aktivizēts
 users.admin=Administrators
 users.restricted=Ierobežots
+users.reserved=Aizņemts
+users.bot=Bots
+users.remote=Attāls
 users.2fa=2FA
 users.repos=Repozitoriji
 users.created=Izveidots
@@ -2676,6 +2830,7 @@ users.list_status_filter.is_prohibit_login=Nav atļauta autorizēšanās
 users.list_status_filter.not_prohibit_login=Atļaut autorizāciju
 users.list_status_filter.is_2fa_enabled=2FA iespējots
 users.list_status_filter.not_2fa_enabled=2FA nav iespējots
+users.details=Lietotāja informācija
 
 emails.email_manage_panel=Lietotāju e-pastu pārvaldība
 emails.primary=Primārais
@@ -2688,6 +2843,7 @@ emails.updated=E-pasts atjaunots
 emails.not_updated=Neizdevās atjaunot pieprasīto e-pasta adresi: %v
 emails.duplicate_active=E-pasta adrese jau ir aktīva citam lietotājam.
 emails.change_email_header=Atjaunot e-pasta rekvizītus
+emails.change_email_text=Vai patiešām vēlaties atjaunot šo e-pasta adresi?
 
 orgs.org_manage_panel=Organizāciju pārvaldība
 orgs.name=Nosaukums
@@ -2701,15 +2857,15 @@ repos.unadopted.no_more=Netika atrasts neviens nepārņemtais repozitorijs
 repos.owner=Īpašnieks
 repos.name=Nosaukums
 repos.private=Privāts
-repos.watches=Vērošana
-repos.stars=Atzīmētās zvaigznītes
-repos.forks=Atdalītie
 repos.issues=Problēmas
 repos.size=Izmērs
+repos.lfs_size=LFS izmērs
 
 packages.package_manage_panel=Pakotņu pārvaldība
 packages.total_size=Kopējais izmērs: %s
 packages.unreferenced_size=Izmērs bez atsauces: %s
+packages.cleanup=Notīrīt novecojušos datus
+packages.cleanup.success=Novecojuši dati veiksmīgi notīrīti
 packages.owner=Īpašnieks
 packages.creator=Izveidotājs
 packages.name=Nosaukums
@@ -2720,10 +2876,12 @@ packages.size=Izmērs
 packages.published=Publicēts
 
 defaulthooks=Noklusētie tīmekļa āķi
+defaulthooks.desc=Tīmekļa āķi automātiski nosūta HTTP POST pieprasījumus serverim, kad iestājas noteikti Gitea notikumi. Šeit pievienotie tīmekļa āķi ir noklusējuma, un tie tiks pievienoti visiem jaunajiem repozitorijiem. Vairāk ir lasāms <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">tīmekļa āķu dokumentācijā</a>.
 defaulthooks.add_webhook=Pievienot noklusēto tīmekļa āķi
 defaulthooks.update_webhook=Mainīt noklusēto tīmekļa āķi
 
 systemhooks=Sistēmas tīmekļa āķi
+systemhooks.desc=Tīmekļa āķi automātiski nosūta HTTP POST pieprasījumus serverim, kad iestājas noteikti Gitea notikumi. Šeit pievienotie tīmekļa āķi tiks izsaukti visiem sistēmas repozitorijiem, tādēļ lūgums apsvērt to iespējamo ietekmi uz veiktspēju. Vairāk ir lasāms <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">tīmekļa āķu dokumentācijā</a>.
 systemhooks.add_webhook=Pievienot sistēmas tīmekļa āķi
 systemhooks.update_webhook=Mainīt sistēmas tīmekļa āķi
 
@@ -2816,17 +2974,18 @@ auths.sspi_default_language=Noklusētā lietotāja valoda
 auths.sspi_default_language_helper=Noklusētā valoda, ko uzstādīt automātiski izveidotajiem lietotājiem, kas izmanto SSPI autentifikācijas veidu. Atstājiet tukšu, ja vēlaties, lai valoda tiktu noteikta automātiski.
 auths.tips=Padomi
 auths.tips.oauth2.general=OAuth2 autentifikācija
+auths.tips.oauth2.general.tip=Kad tiek reģistrēta jauna OAuth2 autentifikācija, atzvanīšanas/pārvirzīšanas URL vajadzētu būt:
 auths.tip.oauth2_provider=OAuth2 pakalpojuma sniedzējs
 auths.tip.bitbucket=Reģistrējiet jaunu OAuth klientu adresē https://bitbucket.org/account/user/<jūsu lietotājvārds>/oauth-consumers/new un piešķiriet tam "Account" - "Read" tiesības
 auths.tip.nextcloud=`Reģistrējiet jaunu OAuth klientu jūsu instances sadāļā "Settings -> Security -> OAuth 2.0 client"`
 auths.tip.dropbox=Izveidojiet jaunu aplikāciju adresē https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Reģistrējiet jaunu aplikāciju adresē https://developers.facebook.com/apps un pievienojiet produktu "Facebook Login"`
 auths.tip.github=Reģistrējiet jaunu aplikāciju adresē https://github.com/settings/applications/new
-auths.tip.gitlab=Reģistrējiet jaunu aplikāciju adresē https://gitlab.com/profile/applications
 auths.tip.google_plus=Iegūstiet OAuth2 klienta pilnvaru no Google API konsoles adresē https://console.developers.google.com/
 auths.tip.openid_connect=Izmantojiet OpenID pieslēgšanās atklāšanas URL (<serveris>/.well-known/openid-configuration), lai norādītu galapunktus
 auths.tip.twitter=Dodieties uz adresi https://dev.twitter.com/apps, izveidojiet lietotni un pārliecinieties, ka ir atzīmēts “Allow this application to be used to Sign in with Twitter”
 auths.tip.discord=Reģistrējiet jaunu aplikāciju adresē https://discordapp.com/developers/applications/me
+auths.tip.gitea=Pievienot jaunu OAuth2 lietojumprogrammu. Dokumentācija ir pieejama https://docs.gitea.com/development/oauth2-provider
 auths.tip.yandex=`Izveidojiet jaunu lietotni adresē https://oauth.yandex.com/client/new. Izvēlieties sekojošas tiesības "Yandex.Passport API" sadaļā: "Access to email address", "Access to user avatar" un "Access to username, first name and surname, gender"`
 auths.tip.mastodon=Norādiet pielāgotu mastodon instances URL, ar kuru vēlaties autorizēties (vai izmantojiet noklusēto)
 auths.edit=Labot autentifikācijas avotu
@@ -2856,6 +3015,7 @@ config.disable_router_log=Atspējot maršrutētāja žurnalizēšanu
 config.run_user=Izpildes lietotājs
 config.run_mode=Izpildes režīms
 config.git_version=Git versija
+config.app_data_path=Lietotnes datu ceļš
 config.repo_root_path=Repozitoriju glabāšanas vieta
 config.lfs_root_path=LFS saknes ceļš
 config.log_file_root_path=Žurnalizēšanas ceļš
@@ -3005,8 +3165,10 @@ monitor.queue.name=Nosaukums
 monitor.queue.type=Veids
 monitor.queue.exemplar=Eksemplāra veids
 monitor.queue.numberworkers=Strādņu skaits
+monitor.queue.activeworkers=Darbojošies strādņi
 monitor.queue.maxnumberworkers=Maksimālais strādņu skaits
 monitor.queue.numberinqueue=Skaits rindā
+monitor.queue.review_add=Pārskatīt/pievienot strādņus
 monitor.queue.settings.title=Pūla iestatījumi
 monitor.queue.settings.desc=Pūls dinamiski tiek palielināts atkarībā no bloķētiem darbiem rindā.
 monitor.queue.settings.maxnumberworkers=Maksimālais strādņu skaits
@@ -3032,6 +3194,8 @@ notices.desc=Apraksts
 notices.op=Op.
 notices.delete_success=Sistēmas paziņojumi ir dzēsti.
 
+self_check.no_problem_found=Pašlaik nav atrasta neviena problēma.
+
 [action]
 create_repo=izveidoja repozitoriju <a href="%s">%s</a>
 rename_repo=pārsauca repozitoriju no <code>%[1]s</code> uz <a href="%[2]s">%[3]s</a>
@@ -3062,7 +3226,7 @@ publish_release=`izveidoja versiju <a href="%[2]s"> "%[4]s" </a> repozitorijā <
 review_dismissed=`noraidīja lietotāja <b>%[4]s</b> recenziju izmaiņu pieprasījumam <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Iemesls:
 create_branch=izveidoja atzaru <a href="%[2]s">%[3]s</a> repozitorijā <a href="%[1]s">%[4]s</a>
-starred_repo=atzīmēja ar zvaigznīti <a href="%[1]s">%[2]s</a>
+starred_repo=pievienoja izlasē <a href="%[1]s">%[2]s</a>
 watched_repo=sāka sekot <a href="%[1]s">%[2]s</a>
 
 [tool]
@@ -3127,6 +3291,7 @@ desc=Pārvaldīt repozitorija pakotnes.
 empty=Pašlaik šeit nav nevienas pakotnes.
 empty.documentation=Papildus informācija par pakotņu reģistru pieejama <a target="_blank" rel="noopener noreferrer" href="%s">dokumentācijā</a>.
 empty.repo=Neparādās augšupielādēta pakotne? Apmeklējiet <a href="%[1]s">pakotņu iestatījumus</a>, lai sasaistītu ar repozitoriju.
+registry.documentation=Vairāk informācija par %s reģistru ir pieejama <a target="_blank" rel="noopener noreferrer" href="%s">dokumentācijā</a>.
 filter.type=Veids
 filter.type.all=Visas
 filter.no_result=Pēc norādītajiem kritērijiem nekas netika atrasts.
@@ -3212,7 +3377,11 @@ pub.install=Lai instalētu Dart pakotni, izpildiet sekojošu komandu:
 pypi.requires=Nepieciešams Python
 pypi.install=Lai instalētu pip pakotni, izpildiet sekojošu komandu:
 rpm.registry=Konfigurējiet šo reģistru no komandrindas:
+rpm.distros.redhat=uz RedHat balstītās operētājsistēmās
+rpm.distros.suse=uz SUSE balstītās operētājsistēmās
 rpm.install=Lai uzstādītu pakotni, ir jāizpilda šī komanda:
+rpm.repository=Repozitorija informācija
+rpm.repository.architectures=Arhitektūras
 rubygems.install=Lai instalētu gem pakotni, izpildiet sekojošu komandu:
 rubygems.install2=vai pievienojiet Gemfile:
 rubygems.dependencies.runtime=Izpildlaika atkarības
@@ -3236,14 +3405,17 @@ settings.delete.success=Pakotne tika izdzēsta.
 settings.delete.error=Neizdevās izdzēst pakotni.
 owner.settings.cargo.title=Cargo reģistra inkdess
 owner.settings.cargo.initialize=Inicializēt indeksu
+owner.settings.cargo.initialize.description=Ir nepieciešams īpašs indeksa Git repozitorijs, lai izmantotu Cargo reģistru. Šīs iespējas izmantošana (atkārtoti) izveidos repozitoriju un automātiski to iestatīs.
 owner.settings.cargo.initialize.error=Neizdevās inicializēt Cargo indeksu: %v
 owner.settings.cargo.initialize.success=Cargo indekss tika veiksmīgi inicializēts.
 owner.settings.cargo.rebuild=Pārbūvēt indeksu
+owner.settings.cargo.rebuild.description=Pārbūvēšana var būt noderīga, ja indekss nav sinhronizēts ar saglabātajām Cargo pakotnēm.
 owner.settings.cargo.rebuild.error=Neizdevās pārbūvēt Cargo indeksu: %v
 owner.settings.cargo.rebuild.success=Cargo indekss tika veiksmīgi pārbūvēts.
 owner.settings.cleanuprules.title=Pārvaldīt notīrīšanas noteikumus
 owner.settings.cleanuprules.add=Pievienot notīrīšanas noteikumu
 owner.settings.cleanuprules.edit=Labot notīrīšanas noteikumu
+owner.settings.cleanuprules.none=Nav pievienoti tīrīšanas noteikumi. Sīkāku informāciju iespējams iegūt dokumentācijā.
 owner.settings.cleanuprules.preview=Notīrīšānas noteikuma priekšskatījums
 owner.settings.cleanuprules.preview.overview=Ir ieplānota %d paku dzēšana.
 owner.settings.cleanuprules.preview.none=Notīrīšanas noteikumam neatbilst neviena pakotne.
@@ -3262,6 +3434,7 @@ owner.settings.cleanuprules.success.update=Notīrīšanas noteikumi tika atjauno
 owner.settings.cleanuprules.success.delete=Notīrīšanas noteikumi tika izdzēsti.
 owner.settings.chef.title=Chef reģistrs
 owner.settings.chef.keypair=Ģenerēt atslēgu pāri
+owner.settings.chef.keypair.description=Atslēgu pāris ir nepieciešams, lai autentificētos Chef reģistrā. Ja iepriekš ir izveidots atslēgu pāris, jauna pāra izveidošana veco atslēgu pāri padarīs nederīgu.
 
 [secrets]
 secrets=Noslēpumi
@@ -3288,6 +3461,7 @@ status.waiting=Gaida
 status.running=Izpildās
 status.success=Pabeigts
 status.failure=Neveiksmīgs
+status.cancelled=Atcelts
 status.skipped=Izlaists
 status.blocked=Bloķēts
 
@@ -3300,11 +3474,12 @@ runners.id=ID
 runners.name=Nosaukums
 runners.owner_type=Veids
 runners.description=Apraksts
-runners.labels=Etiķetes
+runners.labels=Iezīmes
 runners.last_online=Pēdējo reizi tiešsaistē
 runners.runner_title=Izpildītājs
 runners.task_list=Pēdējās darbības, kas izpildītas
-runners.task_list.run=Palaist
+runners.task_list.no_tasks=Vēl nav uzdevumu.
+runners.task_list.run=Izpildīt
 runners.task_list.status=Statuss
 runners.task_list.repository=Repozitorijs
 runners.task_list.commit=Revīzija
@@ -3324,16 +3499,47 @@ runners.status.idle=Dīkstāvē
 runners.status.active=Aktīvs
 runners.status.offline=Bezsaistē
 runners.version=Versija
+runners.reset_registration_token=Atiestatīt reģistrācijas pilnvaru
 runners.reset_registration_token_success=Izpildītāja reģistrācijas pilnvara tika veiksmīgi atiestatīta
 
 runs.all_workflows=Visas darbaplūsmas
 runs.commit=Revīzija
+runs.scheduled=Ieplānots
+runs.pushed_by=iesūtīja
 runs.invalid_workflow_helper=Darbaplūsmas konfigurācijas fails ir kļūdains. Pārbaudiet konfiugrācijas failu: %s
+runs.no_matching_online_runner_helper=Nav pieejami izpildītāji, kas atbilstu šai iezīmei: %s
+runs.actor=Aktors
 runs.status=Statuss
+runs.actors_no_select=Visi aktori
+runs.status_no_select=Visi stāvokļi
+runs.no_results=Netika atrasts nekas atbilstošs.
+runs.no_workflows=Vēl nav nevienas darbplūsmas.
+runs.no_runs=Darbplūsmai vēl nav nevienas izpildes.
+runs.empty_commit_message=(tukšs revīzijas ziņojums)
 
+workflow.disable=Atspējot darbplūsmu
+workflow.disable_success=Darbplūsma '%s' ir veiksmīgi atspējota.
+workflow.enable=Iespējot darbplūsmu
+workflow.enable_success=Darbplūsma '%s' ir veiksmīgi iespējota.
+workflow.disabled=Darbplūsma ir atspējota.
 
 need_approval_desc=Nepieciešams apstiprinājums, lai izpildītu izmaiņu pieprasījumu darbaplūsmas no atdalītiem repozitorijiem.
 
+variables=Mainīgie
+variables.management=Mainīgo pārvaldība
+variables.creation=Pievienot mainīgo
+variables.none=Vēl nav neviena mainīgā.
+variables.deletion=Noņemt mainīgo
+variables.deletion.description=Mainīgā noņemšana ir neatgriezeniska un nav atsaucama. Vai turpināt?
+variables.description=Mainīgie tiks padoti noteiktām darbībām, un citādāk tos nevar nolasīt.
+variables.id_not_exist=Mainīgais ar identifikatoru %d nepastāv.
+variables.edit=Labot mainīgo
+variables.deletion.failed=Neizdevās noņemt mainīgo.
+variables.deletion.success=Mainīgais tika noņemts.
+variables.creation.failed=Neizdevās pievienot mainīgo.
+variables.creation.success=Mainīgais "%s" tika pievienots.
+variables.update.failed=Neizdevās labot mainīgo.
+variables.update.success=Mainīgais tika labots.
 
 [projects]
 type-1.display_name=Individuālais projekts
@@ -3341,6 +3547,11 @@ type-2.display_name=Repozitorija projekts
 type-3.display_name=Organizācijas projekts
 
 [git.filemode]
+changed_filemode=%[1]s → %[2]s
 ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Direktorija
+normal_file=Parasts fails
+executable_file=Izpildāmais fails
 symbolic_link=Simboliska saite
+submodule=Apakšmodulis
 
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index 43265c9c31..6b5122a86f 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -115,6 +115,15 @@ concept_user_organization=Organisatie
 
 name=Naam
 
+filter=Filter
+filter.is_archived=Gearchiveerd
+filter.is_template=Sjabloon
+filter.public=Publiek
+filter.private=Prive
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -253,7 +262,6 @@ collaborative_repos=Gedeelde repositories
 my_orgs=Mijn organisaties
 my_mirrors=Mijn spiegels
 view_home=Bekijk %s
-search_repos=Zoek een repository…
 filter=Andere filters
 filter_by_team_repositories=Filter op team repositories
 feed_of=`Feed van "%s"`
@@ -274,15 +282,7 @@ issues.in_your_repos=In uw repositories
 repos=Repositories
 users=Gebruikers
 organizations=Organisaties
-search=Zoeken
 code=Code
-search.fuzzy=Vergelijkbaar
-search.match=Overeenkomst
-code_search_unavailable=Er is momenteel geen code zoekfunctie beschikbaar. Neem contact op met uw sitebeheerder.
-repo_no_results=Er zijn geen overeenkomende repositories gevonden.
-user_no_results=Er zijn geen overeenkomende gebruikers gevonden.
-org_no_results=Er zijn geen overeenkomende organisaties gevonden.
-code_no_results=Geen broncode gevonden in overeenstemming met uw zoekterm.
 code_last_indexed_at=Laatst geïndexeerd %s
 
 [auth]
@@ -296,7 +296,6 @@ remember_me=Onthoud dit apparaat
 forgot_password_title=Wachtwoord vergeten
 forgot_password=Wachtwoord vergeten?
 sign_up_now=Een account nodig? Meld u nu aan.
-confirmation_mail_sent_prompt=Een nieuwe bevestigingsmail is gestuurd naar <b>%s</b>. De mail moet binnen %s worden bevestigd om je registratie te voltooien.
 must_change_password=Uw wachtwoord wijzigen
 allow_password_change=Verplicht de gebruiker om zijn/haar wachtwoord te wijzigen (aanbevolen)
 reset_password_mail_sent_prompt=Een bevestigingsmail is verstuurd naar <b>%s</b>. Controleer uw inbox in de volgende %s om het herstel van uw account te voltooien.
@@ -489,6 +488,7 @@ auth_failed=Verificatie mislukt: %v
 
 target_branch_not_exist=Doel branch bestaat niet
 
+
 [user]
 change_avatar=Wijzig je profielfoto…
 repositories=repositories
@@ -505,6 +505,7 @@ user_bio=Biografie
 disabled_public_activity=Deze gebruiker heeft de publieke zichtbaarheid van de activiteit uitgeschakeld.
 
 
+
 [settings]
 profile=Profiel
 account=Account
@@ -632,7 +633,6 @@ gpg_invalid_token_signature=De opgegeven GPG-sleutel, handtekening en token kome
 gpg_token_required=U moet een handtekening opgeven voor de onderstaande token
 gpg_token=Token
 gpg_token_help=U kunt een handtekening genereren met:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Gepantserde GPG-handtekening
 key_signature_gpg_placeholder=Begint met '-----BEGIN PGP SIGNATURE-----'
 ssh_key_verified=Geverifieerde sleutel
@@ -784,7 +784,6 @@ already_forked=Je hebt %s al geforked
 fork_to_different_account=Fork naar een ander account
 fork_visibility_helper=De zichtbaarheid van een geforkte repository kan niet worden veranderd.
 use_template=Gebruik dit sjabloon
-clone_in_vsc=Kloon in VS Code
 download_zip=ZIP downloaden
 download_tar=TAR.GZ downloaden
 download_bundle=BUNDLE downloaden
@@ -1048,8 +1047,6 @@ editor.revert=%s ongedaan maken op:
 commits.desc=Bekijk de broncode-wijzigingsgeschiedenis.
 commits.commits=Commits
 commits.nothing_to_compare=Deze branches zijn gelijk.
-commits.search=Zoek commits…
-commits.find=Zoek
 commits.search_all=Alle branches
 commits.author=Auteur
 commits.message=Bericht
@@ -1094,7 +1091,6 @@ projects.type.basic_kanban=Basis Kanban
 projects.type.bug_triage=Bug Triage
 projects.template.desc=Project sjabloon
 projects.template.desc_helper=Selecteer een projecttemplate om aan de slag te gaan
-projects.type.uncategorized=Ongecategoriseerd
 projects.column.edit_title=Naam
 projects.column.new_title=Naam
 projects.column.color=Kleur
@@ -1405,7 +1401,6 @@ pulls.compare_compare=trekken van
 pulls.switch_comparison_type=Wissel vergelijking type
 pulls.switch_head_and_base=Verwissel hoofd en basis
 pulls.filter_branch=Filter branch
-pulls.no_results=Geen resultaten gevonden.
 pulls.nothing_to_compare=Deze branches zijn gelijk. Er is geen pull-aanvraag nodig.
 pulls.nothing_to_compare_and_allow_empty_pr=Deze branches zijn gelijk. Deze pull verzoek zal leeg zijn.
 pulls.has_pull_request=`Een pull-verzoek tussen deze branches bestaat al: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1618,13 +1613,7 @@ activity.git_stats_and_deletions=en
 activity.git_stats_deletion_1=%d verwijdering
 activity.git_stats_deletion_n=%d verwijderingen
 
-search=Zoek
-search.search_repo=Zoek repository
-search.fuzzy=Vergelijkbaar
-search.match=Overeenkomst
-search.results=Zoek resultaat voor "%s" in <a href="%s">%s</a>
-search.code_no_results=Geen broncode gevonden die aan uw zoekterm voldoet.
-search.code_search_unavailable=Er is momenteel geen code zoekfunctie beschikbaar. Neem contact op met uw sitebeheerder.
+contributors.contribution_type.commits=Commits
 
 settings=Instellingen
 settings.desc=In de instellingen kan je de instellingen van de repository aanpassen
@@ -1702,7 +1691,6 @@ settings.delete_collaborator=Verwijder
 settings.collaborator_deletion=Verwijder medewerker
 settings.collaborator_deletion_desc=Het verwijderen van een collaborator zal hun toegang tot deze repository intrekken. Doorgaan?
 settings.remove_collaborator_success=De medewerker is verwijderd.
-settings.search_user_placeholder=Zoek gebruiker…
 settings.org_not_allowed_to_be_collaborator=Organisaties kunnen niet worden toegevoegd als een medewerker.
 settings.change_team_access_not_allowed=Het veranderen van team toegang voor de repository is beperkt tot de organisatie eigenaar
 settings.team_not_in_organization=Het team zit niet in dezelfde organisatie als de repository
@@ -1710,7 +1698,6 @@ settings.teams=Teams
 settings.add_team=Team toevoegen
 settings.add_team_duplicate=Team heeft al de repository
 settings.add_team_success=Het team heeft nu toegang tot de repository.
-settings.search_team=Zoek team…
 settings.change_team_permission_tip=Teammachtiging is ingesteld op de team-instellingspagina en kan niet per repository worden gewijzigd
 settings.delete_team_tip=Dit team heeft toegang tot alle repositories en kan niet verwijderd worden
 settings.remove_team_success=De toegang van het team tot de repository is verwijderd.
@@ -1842,9 +1829,7 @@ settings.protect_whitelist_committers=Whitelist Beperkte Push
 settings.protect_whitelist_committers_desc=Alleen gewhiteliste gebruikers of teams mogen pushen naar deze branch (maar geen force push).
 settings.protect_whitelist_deploy_keys=Whitelist deploy sleutels met schrijftoegang om te pushen.
 settings.protect_whitelist_users=Toegestane gebruikers voor push:
-settings.protect_whitelist_search_users=Zoek gebruiker…
 settings.protect_whitelist_teams=Toegestane teams voor push:
-settings.protect_whitelist_search_teams=Zoek teams…
 settings.protect_merge_whitelist_committers=Samenvoegen whitelist inschakelen
 settings.protect_merge_whitelist_committers_desc=Sta alleen gebruikers of teams van de whitelist toe om pull requests samen te voegen met deze branch.
 settings.protect_merge_whitelist_users=Toegestane gebruikers voor samenvoegen:
@@ -2031,6 +2016,8 @@ topic.count_prompt=Je kunt niet meer dan 25 onderwerpen selecteren
 
 
 
+[graphs]
+
 [org]
 org_name_holder=Organisatienaam
 org_full_name_holder=Volledige naam organisatie
@@ -2118,7 +2105,6 @@ teams.write_permission_desc=Dit team heeft <strong>Schrijf</strong> rechten: led
 teams.admin_permission_desc=Dit team heeft <strong>beheersrechten</strong>: leden kunnen van en naar teamrepositories pullen, pushen, en er medewerkers aan toevoegen.
 teams.create_repo_permission_desc=Daarnaast verleent dit team <strong>Maak repository</strong> permissie: leden kunnen nieuwe repositories maken in de organisatie.
 teams.repositories=Teamrepositories
-teams.search_repo_placeholder=Repository zoeken…
 teams.remove_all_repos_title=Verwijder alle team repositories
 teams.remove_all_repos_desc=Dit zal alle repositories uit het team verwijderen.
 teams.add_all_repos_title=Voeg alle repositories toe
@@ -2140,6 +2126,8 @@ repositories=Repositories
 authentication=Authenticatie bronnen
 emails=Gebruikeremails
 config=Configuratie
+config_summary=Overzicht
+config_settings=Instellingen
 notices=Systeem aankondigingen
 monitor=Bijhouden
 first_page=Eerste
@@ -2147,7 +2135,6 @@ last_page=Laatste
 total=Totaal: %d
 
 dashboard.statistic=Overzicht
-dashboard.operations=Onderhoudswerkzaamheden
 dashboard.system_status=Systeemtatus
 dashboard.operation_name=Bewerking naam
 dashboard.operation_switch=Omschakelen
@@ -2277,9 +2264,6 @@ repos.unadopted.no_more=Geen niet-geadopteerde repositories meer gevonden
 repos.owner=Eigenaar
 repos.name=Naam
 repos.private=Prive
-repos.watches=Volgers
-repos.stars=Sterren
-repos.forks=Forks
 repos.issues=Kwesties
 repos.size=Grootte
 
@@ -2360,7 +2344,6 @@ auths.tip.nextcloud=`Registreer een nieuwe OAuth consument op je installatie met
 auths.tip.dropbox=Maak een nieuwe applicatie aan op https://www.dropbox.com/developers/apps
 auths.tip.facebook=Registreer een nieuwe applicatie op https://developers.facebook.com/apps en voeg het product "Facebook Login" toe
 auths.tip.github=Registreer een nieuwe OAuth toepassing op https://github.com/settings/applications/new
-auths.tip.gitlab=Registreer een nieuwe applicatie op https://gitlab.com/profile/applicaties
 auths.tip.google_plus=Verkrijg OAuth2 client referenties van de Google API console op https://console.developers.google.com/
 auths.tip.openid_connect=Gebruik de OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) om de eindpunten op te geven
 auths.tip.yandex=`Maak een nieuwe applicatie aan op https://oauth.yandex.com/client/new. Selecteer de volgende machtigingen van de "Yandex". assport API sectie: "Toegang tot e-mailadres", "Toegang tot avatar" en "Toegang tot gebruikersnaam, voornaam en achternaam, geslacht"`
@@ -2537,6 +2520,7 @@ notices.desc=Beschrijving
 notices.op=Op.
 notices.delete_success=De systeemmeldingen zijn verwijderd.
 
+
 [action]
 create_repo=repository aangemaakt in <a href="%s">%s</a>
 rename_repo=hernoemde repository van <code>%[1]s</code> naar <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index d713110a72..a1d7e95842 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -113,6 +113,14 @@ concept_user_organization=Organizacja
 
 name=Nazwa
 
+filter.is_archived=Zarchiwizowane
+filter.is_template=Szablon
+filter.public=Publiczne
+filter.private=Prywatne
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -251,7 +259,6 @@ collaborative_repos=Wspólne repozytoria
 my_orgs=Moje organizacje
 my_mirrors=Moje kopie lustrzane
 view_home=Zobacz %s
-search_repos=Znajdź repozytorium…
 filter=Inne filtry
 filter_by_team_repositories=Filtruj według repozytoriów zespołu
 feed_of=`Kanał "%s"`
@@ -272,14 +279,7 @@ issues.in_your_repos=W Twoich repozytoriach
 repos=Repozytoria
 users=Użytkownicy
 organizations=Organizacje
-search=Szukaj
 code=Kod
-search.fuzzy=Fuzzy
-search.match=Dopasuj
-repo_no_results=Nie znaleziono pasujących repozytoriów.
-user_no_results=Nie znaleziono pasującego użytkowników.
-org_no_results=Nie znaleziono pasujących organizacji.
-code_no_results=Nie znaleziono kodu źródłowego odpowiadającego Twojej frazie wyszukiwania.
 code_last_indexed_at=Ostatnio indeksowane %s
 
 [auth]
@@ -292,7 +292,6 @@ remember_me=Zapamiętaj to urządzenie
 forgot_password_title=Zapomniałem hasła
 forgot_password=Zapomniałeś hasła?
 sign_up_now=Potrzebujesz konta? Zarejestruj się teraz.
-confirmation_mail_sent_prompt=Nowy email aktywacyjny został wysłany na adres <b>%s</b>. Sprawdź swoją skrzynkę odbiorczą w ciągu %s aby dokończyć proces rejestracji.
 must_change_password=Zaktualizuj swoje hasło
 allow_password_change=Użytkownik musi zmienić hasło (zalecane)
 reset_password_mail_sent_prompt=E-mail potwierdzający został wysłany na adres <b>%s</b>. Sprawdź swoją skrzynkę odbiorczą w przeciągu %s, aby ukończyć proces odzyskiwania konta.
@@ -474,6 +473,7 @@ auth_failed=Uwierzytelnienie się nie powiodło: %v
 
 target_branch_not_exist=Gałąź docelowa nie istnieje.
 
+
 [user]
 change_avatar=Zmień swój awatar…
 repositories=Repozytoria
@@ -490,6 +490,7 @@ user_bio=Biografia
 disabled_public_activity=Ten użytkownik wyłączył publiczne wyświetlanie jego aktywności.
 
 
+
 [settings]
 profile=Profil
 account=Konto
@@ -596,7 +597,6 @@ gpg_invalid_token_signature=Podany klucz GPG, podpis i token nie pasują lub tok
 gpg_token_required=Musisz podać podpis poniższego tokenu
 gpg_token=Token
 gpg_token_help=Możesz wygenerować podpis za pomocą:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Wzmocniony podpis GPG
 key_signature_gpg_placeholder=Zaczyna się od '-----BEGIN PGP SIGNATURE-----'
 ssh_key_verified=Zweryfikowany klucz
@@ -740,7 +740,6 @@ fork_repo=Forkuj repozytorium
 fork_from=Forkuj z
 fork_visibility_helper=Widoczność sforkowanego repozytorium nie może być zmieniona.
 use_template=Użyj tego szablonu
-clone_in_vsc=Klonuj w VS Code
 download_zip=Pobierz ZIP
 download_tar=Pobierz TAR.GZ
 download_bundle=Pobierz BUNDLE
@@ -973,8 +972,6 @@ editor.require_signed_commit=Gałąź wymaga podpisanych commitów
 
 commits.desc=Przeglądaj historię zmian kodu źródłowego.
 commits.commits=Commity
-commits.search=Przeszukaj commity…
-commits.find=Szukaj
 commits.search_all=Wszystkie gałęzie
 commits.author=Autor
 commits.message=Wiadomość
@@ -1010,7 +1007,6 @@ projects.type.basic_kanban=Basic Kanban
 projects.type.bug_triage=Bug Triage
 projects.template.desc=Szablon projektu
 projects.template.desc_helper=Wybierz szablon projektu do rozpoczęcia
-projects.type.uncategorized=Bez kategorii
 projects.column.edit_title=Nazwa
 projects.column.new_title=Nazwa
 projects.column.color=Kolor
@@ -1279,7 +1275,6 @@ pulls.compare_changes_desc=Wybierz gałąź, do której chcesz scalić oraz gał
 pulls.compare_base=scal do
 pulls.compare_compare=ściągnij z
 pulls.filter_branch=Filtruj branch
-pulls.no_results=Nie znaleziono wyników.
 pulls.nothing_to_compare=Te gałęzie są sobie równe. Nie ma potrzeby tworzyć Pull Requesta.
 pulls.nothing_to_compare_and_allow_empty_pr=Te gałęzie są równe. Ten PR będzie pusty.
 pulls.create=Utwórz Pull Request
@@ -1467,12 +1462,7 @@ activity.git_stats_and_deletions=i
 activity.git_stats_deletion_1=%d usunięcie
 activity.git_stats_deletion_n=%d usunięć
 
-search=Szukaj
-search.search_repo=Przeszukaj repozytorium
-search.fuzzy=Fuzzy
-search.match=Dopasuj
-search.results=Wyniki wyszukiwania dla "%s" w <a href="%s">%s</a>
-search.code_no_results=Nie znaleziono kodu źródłowego odpowiadającego Twojej frazie wyszukiwania.
+contributors.contribution_type.commits=Commity
 
 settings=Ustawienia
 settings.desc=Ustawienia to miejsce, w którym możesz zmieniać parametry repozytorium
@@ -1583,7 +1573,6 @@ settings.delete_collaborator=Usuń
 settings.collaborator_deletion=Usuń współpracownika
 settings.collaborator_deletion_desc=Usunięcie współpracownika odbierze mu dostęp do tego repozytorium. Kontynuować?
 settings.remove_collaborator_success=Usunięto użytkownika.
-settings.search_user_placeholder=Szukaj użytkownika…
 settings.org_not_allowed_to_be_collaborator=Organizacji nie można dodać jako współpracownika.
 settings.change_team_access_not_allowed=Zmiana dostępu zespołu do repozytorium zostało zastrzeżone do właściciela organizacji
 settings.team_not_in_organization=Zespół nie jest w tej samej organizacji co repozytorium
@@ -1591,7 +1580,6 @@ settings.teams=Zespoły
 settings.add_team=Dodaj zespół
 settings.add_team_duplicate=Zespół już posiada repozytorium
 settings.add_team_success=Zespół ma teraz dostęp do repozytorium.
-settings.search_team=Szukaj zespołu…
 settings.change_team_permission_tip=Uprawnienia zespołu ustawione są konfigurowane na stronie ustawień zespołu i nie mogą być zmieniane dla pojedynczych repozytoriów
 settings.delete_team_tip=Ten zespół ma dostęp do wszystkich repozytoriów i nie może zostać usunięty
 settings.remove_team_success=Dostęp zespołu do repozytorium został usunięty.
@@ -1707,9 +1695,7 @@ settings.protect_whitelist_committers=Wypychanie ograniczone białą listą
 settings.protect_whitelist_committers_desc=Tylko dopuszczeni użytkownicy oraz zespoły będą miały możliwość wypychania zmian do tej gałęzi (oprócz wymuszenia wypchnięcia).
 settings.protect_whitelist_deploy_keys=Dozwolona lista kluczy wdrożeniowych z uprawnieniem zapisu do push'a.
 settings.protect_whitelist_users=Użytkownicy dopuszczeni do wypychania:
-settings.protect_whitelist_search_users=Szukaj użytkowników…
 settings.protect_whitelist_teams=Zespoły dopuszczone do wypychania:
-settings.protect_whitelist_search_teams=Szukaj zespołów…
 settings.protect_merge_whitelist_committers=Włącz dopuszczenie scalania
 settings.protect_merge_whitelist_committers_desc=Zezwól jedynie dopuszczonym użytkownikom lub zespołom na scalanie Pull Requestów w tej gałęzi.
 settings.protect_merge_whitelist_users=Użytkownicy dopuszczeni do scalania:
@@ -1899,6 +1885,8 @@ error.csv.too_large=Nie można wyświetlić tego pliku, ponieważ jest on zbyt d
 error.csv.unexpected=Nie można renderować tego pliku, ponieważ zawiera nieoczekiwany znak w wierszu %d i kolumnie %d.
 error.csv.invalid_field_count=Nie można renderować tego pliku, ponieważ ma nieprawidłową liczbę pól w wierszu %d.
 
+[graphs]
+
 [org]
 org_name_holder=Nazwa organizacji
 org_full_name_holder=Pełna nazwa organizacji
@@ -1989,7 +1977,6 @@ teams.write_permission_desc=Ten zespół udziela dostępu <strong>z zapisem</str
 teams.admin_permission_desc=Ten zespół udziela dostępu <strong>administratora</strong>: członkowie mogą wyświetlać i wypychać zmiany oraz dodawać współpracowników do repozytoriów zespołu.
 teams.create_repo_permission_desc=Dodatkowo, ten zespół otrzyma uprawnienie <strong>Tworzenie repozytoriów</strong>: jego członkowie mogą tworzyć nowe repozytoria w organizacji.
 teams.repositories=Repozytoria zespołu
-teams.search_repo_placeholder=Szukaj repozytorium…
 teams.remove_all_repos_title=Usuń wszystkie repozytoria zespołu
 teams.remove_all_repos_desc=Usunie to wszystkie repozytoria przypisane do zespołu.
 teams.add_all_repos_title=Dodaj wszystkie repozytoria
@@ -2014,6 +2001,8 @@ hooks=Weebhook'i
 authentication=Źródła uwierzytelniania
 emails=Emaile użytkowników
 config=Konfiguracja
+config_summary=Podsumowanie
+config_settings=Ustawienia
 notices=Powiadomienia systemu
 monitor=Monitorowanie
 first_page=Pierwsza
@@ -2021,7 +2010,6 @@ last_page=Ostatnia
 total=Ogółem: %d
 
 dashboard.statistic=Podsumowanie
-dashboard.operations=Operacje konserwacji
 dashboard.system_status=Status strony
 dashboard.operation_name=Nazwa operacji
 dashboard.operation_switch=Przełącz
@@ -2152,9 +2140,6 @@ repos.unadopted.no_more=Nie znaleziono więcej nieprzyjętych repozytoriów
 repos.owner=Właściciel
 repos.name=Nazwa
 repos.private=Prywatne
-repos.watches=Obserwujących
-repos.stars=Polubienia
-repos.forks=Forki
 repos.issues=Zgłoszenia
 repos.size=Rozmiar
 
@@ -2243,7 +2228,6 @@ auths.tip.nextcloud=`Zarejestruj nowego klienta OAuth w swojej instancji za pomo
 auths.tip.dropbox=Stwórz nową aplikację na https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Zarejestruj nową aplikację na https://developers.facebook.com/apps i dodaj produkt "Facebook Login"`
 auths.tip.github=Zarejestruj nową aplikację OAuth na https://github.com/settings/applications/new
-auths.tip.gitlab=Zarejestruj nową aplikację na https://gitlab.com/profile/applications
 auths.tip.google_plus=Uzyskaj dane uwierzytelniające klienta OAuth2 z konsoli Google API na https://console.developers.google.com/
 auths.tip.openid_connect=Użyj adresu URL OpenID Connect Discovery (<server>/.well-known/openid-configuration), aby określić punkty końcowe
 auths.tip.twitter=Przejdź na https://dev.twitter.com/apps, stwórz aplikację i upewnij się, że opcja “Allow this application to be used to Sign in with Twitter” jest włączona
@@ -2425,6 +2409,7 @@ notices.desc=Opis
 notices.op=Operacja
 notices.delete_success=Powiadomienia systemu zostały usunięte.
 
+
 [action]
 create_repo=tworzy repozytorium <a href="%s">%s</a>
 rename_repo=zmienia nazwę repozytorium <code>%[1]s</code> na <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index cf5fd0055c..45f1c3b3f8 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -90,6 +90,7 @@ remove=Remover
 remove_all=Excluir todos
 remove_label_str=`Remover item "%s"`
 edit=Editar
+view=Visualizar
 
 enabled=Habilitado
 disabled=Desabilitado
@@ -97,6 +98,7 @@ locked=Bloqueado
 
 copy=Copiar
 copy_url=Copiar URL
+copy_hash=Copiar hash
 copy_content=Copiar conteúdo
 copy_branch=Copiar nome do branch
 copy_success=Copiado!
@@ -109,6 +111,7 @@ loading=Carregando…
 
 error=Erro
 error404=A página que você está tentando acessar <strong>não existe</strong> ou <strong>você não está autorizado</strong> a visualizá-la.
+go_back=Voltar
 
 never=Nunca
 unknown=Desconhecido
@@ -119,6 +122,7 @@ pin=Fixar
 unpin=Desfixar
 
 artifacts=Artefatos
+confirm_delete_artifact=Tem certeza que deseja excluir o artefato '%s' ?
 
 archived=Arquivado
 
@@ -137,6 +141,15 @@ confirm_delete_selected=Confirma a exclusão de todos os itens selecionados?
 name=Nome
 value=Valor
 
+filter=Filtro
+filter.is_archived=Arquivado
+filter.is_template=Template
+filter.public=Pública
+filter.private=Privado
+
+
+[search]
+
 [aria]
 navbar=Barra de navegação
 footer=Rodapé
@@ -309,7 +322,6 @@ collaborative_repos=Repositórios colaborativos
 my_orgs=Minhas organizações
 my_mirrors=Meus espelhos
 view_home=Ver %s
-search_repos=Encontre um repositório…
 filter=Outros filtros
 filter_by_team_repositories=Filtrar por repositórios da equipe
 feed_of=`Feed de "%s"`
@@ -330,20 +342,8 @@ issues.in_your_repos=Em seus repositórios
 repos=Repositórios
 users=Usuários
 organizations=Organizações
-search=Pesquisar
 go_to=Ir para
 code=Código
-search.type.tooltip=Tipo de pesquisa
-search.fuzzy=Similar
-search.fuzzy.tooltip=Incluir resultados que sejam próximos ao termo de busca
-search.match=Correspondência
-search.match.tooltip=Incluir somente resultados que correspondam exatamente ao termo de busca
-code_search_unavailable=A pesquisa por código não está disponível no momento. Entre em contato com o administrador do site.
-repo_no_results=Nenhum repositório correspondente foi encontrado.
-user_no_results=Nenhum usuário correspondente foi encontrado.
-org_no_results=Nenhuma organização correspondente foi encontrada.
-code_no_results=Nenhum código-fonte correspondente ao seu termo de pesquisa foi encontrado.
-code_search_results=`Resultados da pesquisa por: "%s"`
 code_last_indexed_at=Última indexação %s
 relevant_repositories_tooltip=Repositórios que são forks ou que não possuem tópico, nem ícone e nem descrição estão ocultos.
 relevant_repositories=Apenas repositórios relevantes estão sendo mostrados, <a href="%s">mostrar resultados não filtrados</a>.
@@ -356,11 +356,11 @@ disable_register_prompt=Cadastro está desabilitado. Entre em contato com o admi
 disable_register_mail=E-mail de confirmação de cadastro está desabilitado.
 manual_activation_only=Entre em contato com o administrador do site para concluir a ativação.
 remember_me=Lembrar deste Dispositivo
+remember_me.compromised=O token de login não é mais válido, o que pode indicar uma conta comprometida. Por favor, verifique a sua conta por atividades incomuns.
 forgot_password_title=Esqueci minha senha
 forgot_password=Esqueceu sua senha?
 sign_up_now=Precisa de uma conta? Cadastre-se agora.
 sign_up_successful=A conta foi criada com sucesso. Bem-vindo!
-confirmation_mail_sent_prompt=Um novo e-mail de confirmação foi enviado para <b>%s</b>. Por favor, verifique sua caixa de e-mail nas próximas %s horas para finalizar o processo de cadastro.
 must_change_password=Redefina sua senha
 allow_password_change=Exigir que o usuário redefina a senha (recomendado)
 reset_password_mail_sent_prompt=Um e-mail de confirmação foi enviado para <b>%s</b>. Por favor, verifique sua caixa de entrada dentro do(s) próximo(s) %s para concluir o processo de recuperação de conta.
@@ -417,6 +417,7 @@ authorization_failed_desc=A autorização falhou porque detectamos uma solicita
 sspi_auth_failed=Falha de autenticação SSPI
 password_pwned=A senha que você escolheu faz parte de uma <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">lista de senhas roubadas</a> expostas anteriormente em violações de dados. Tente novamente com uma senha diferente e considere alterar essa senha em outro lugar também.
 password_pwned_err=Não foi possível concluir a requisição ao HaveIBeenPwned
+last_admin=Você não pode remover o último administrador. Deve haver pelo menos um administrador.
 
 [mail]
 view_it_on=Veja em %s
@@ -582,6 +583,8 @@ org_still_own_packages=Esta organização ainda possui pacotes, exclua-os primei
 
 target_branch_not_exist=O branch de destino não existe.
 
+admin_cannot_delete_self=Você não pode excluir você mesmo quando você é um administrador. Por favor, remova seus privilégios de administrador primeiro.
+
 [user]
 change_avatar=Altere seu avatar...
 joined_on=Inscreveu-se em %s
@@ -607,6 +610,7 @@ form.name_reserved=O nome de usuário "%s" está reservado.
 form.name_pattern_not_allowed=O padrão de "%s" não é permitido em um nome de usuário.
 form.name_chars_not_allowed=Nome de usuário "%s" contém caracteres inválidos.
 
+
 [settings]
 profile=Perfil
 account=Conta
@@ -751,7 +755,6 @@ gpg_invalid_token_signature=A chave GPG fornecida, a assinatura ou o token não
 gpg_token_required=Você tem que fornecer uma assinatura para o token abaixo
 gpg_token=Token
 gpg_token_help=Você pode gerar uma assinatura usando:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Assinatura GPG blindada
 key_signature_gpg_placeholder=Começa com '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=A chave GPG "%s" foi validada.
@@ -858,6 +861,7 @@ revoke_oauth2_grant_description=Revogando o acesso para este aplicativo de terce
 revoke_oauth2_grant_success=Acesso revogado com sucesso.
 
 twofa_desc=Autenticação de dois fatores melhora a segurança de sua conta.
+twofa_recovery_tip=Se você perder o seu dispositivo, você será capaz de usar uma chave de recuperação de uso único para recuperar o acesso à sua conta.
 twofa_is_enrolled=Sua conta está atualmente <strong>habilitada</strong> com autenticação de dois fatores.
 twofa_not_enrolled=Sua conta não está atualmente inscrita para a autenticação em duas etapas.
 twofa_disable=Desabilitar a autenticação de dois fatores
@@ -880,6 +884,8 @@ webauthn_register_key=Adicionar chave de segurança
 webauthn_nickname=Apelido
 webauthn_delete_key=Remover chave de segurança
 webauthn_delete_key_desc=Se você remover uma chave de segurança, não poderá mais entrar com ela. Continuar?
+webauthn_key_loss_warning=Se você perder suas chaves de segurança, perderá o acesso à sua conta.
+webauthn_alternative_tip=Você pode querer configurar um método de autenticação adicional.
 
 manage_account_links=Gerenciar contas vinculadas
 manage_account_links_desc=Estas contas externas estão vinculadas a sua conta de Gitea.
@@ -916,6 +922,7 @@ visibility.private=Privada
 visibility.private_tooltip=Visível apenas para membros das organizações às quais você se associou
 
 [repo]
+new_repo_helper=Um repositório contém todos os arquivos do projeto, inclusive o histórico de revisões. Já está hospedando um em outro lugar? <a href="%s">Migre o repositório.</a>
 owner=Proprietário
 owner_helper=Algumas organizações podem não aparecer no menu devido a um limite de contagem dos repositórios.
 repo_name=Nome do repositório
@@ -936,9 +943,10 @@ fork_from=Fork de
 already_forked=Você já fez o fork de %s
 fork_to_different_account=Faça um fork para uma conta diferente
 fork_visibility_helper=A visibilidade do fork de um repositório não pode ser alterada.
+fork_branch=Branch a ser clonado para o fork
+all_branches=Todos os branches
 fork_no_valid_owners=Não é possível fazer um fork desse repositório porque não há proprietários validos.
 use_template=Usar este modelo
-clone_in_vsc=Clonar no VS Code
 download_zip=Baixar ZIP
 download_tar=Baixar TAR.GZ
 download_bundle=Baixar PACOTE
@@ -971,6 +979,7 @@ mirror_prune=Varrer
 mirror_prune_desc=Remover referências obsoletas de controle remoto
 mirror_interval=Intervalo de espelhamento (unidades válidas são 'h', 'm', ou 's'). O desabilita a sincronização automática. (Intervalo mínimo: %s)
 mirror_interval_invalid=O intervalo do espelhamento não é válido.
+mirror_sync=sincronizado
 mirror_sync_on_commit=Sincronizar quando commits forem enviados
 mirror_address=Clonar de URL
 mirror_address_desc=Coloque todas as credenciais necessárias na seção de autorização.
@@ -1016,6 +1025,7 @@ desc.public=Público
 desc.template=Template
 desc.internal=Interno
 desc.archived=Arquivado
+desc.sha256=SHA256
 
 template.items=Itens do modelo
 template.git_content=Conteúdo Git (Branch padrão)
@@ -1166,6 +1176,7 @@ audio_not_supported_in_browser=Seu navegador não suporta a tag 'audio' do HTML5
 stored_lfs=Armazenado com Git LFS
 symbolic_link=Link simbólico
 executable_file=Arquivo executável
+generated=Gerado
 commit_graph=Gráfico de commits
 commit_graph.select=Selecionar branches
 commit_graph.hide_pr_refs=Esconder Pull Requests
@@ -1252,9 +1263,7 @@ commits.desc=Veja o histórico de alterações do código de fonte.
 commits.commits=Commits
 commits.no_commits=Nenhum commit em comum. "%s" e "%s" tem históricos completamente diferentes.
 commits.nothing_to_compare=Estes branches são iguais.
-commits.search=Pesquisar commits...
 commits.search.tooltip=Você pode prefixar as palavras-chave com "author:" (autor da mudança), "committer:" (autor do commit), "after:" (depois) ou "before:" (antes). Por exemplo: "revert author:Ana before:2019-01-13".\
-commits.find=Pesquisar
 commits.search_all=Todos os branches
 commits.author=Autor
 commits.message=Mensagem
@@ -1266,6 +1275,7 @@ commits.signed_by_untrusted_user=Assinado por usuário não confiável
 commits.signed_by_untrusted_user_unmatched=Assinado por usuário não confiável que não corresponde ao autor da submissão
 commits.gpg_key_id=ID da chave GPG
 commits.ssh_key_fingerprint=Impressão Digital da Chave SSH
+commits.view_path=Visualizar neste ponto do histórico
 
 commit.operations=Operações
 commit.revert=Reverter
@@ -1304,7 +1314,6 @@ projects.type.basic_kanban=Kanban básico
 projects.type.bug_triage=Triagem de Bugs
 projects.template.desc=Modelo de projeto
 projects.template.desc_helper=Selecione um modelo de projeto para começar
-projects.type.uncategorized=Sem categoria
 projects.column.edit=Editar coluna
 projects.column.edit_title=Nome
 projects.column.new_title=Nome
@@ -1312,10 +1321,7 @@ projects.column.new_submit=Criar coluna
 projects.column.new=Nova Coluna
 projects.column.set_default=Atribuir como padrão
 projects.column.set_default_desc=Definir esta coluna como padrão para pull e issues sem categoria
-projects.column.unset_default=Desatribuir padrão
-projects.column.unset_default_desc=Desatribuir esta coluna como padrão
 projects.column.delete=Excluir coluna
-projects.column.deletion_desc=Excluir uma coluna do projeto move todas as issues relacionadas para 'Sem categoria'. Continuar?
 projects.column.color=Cor
 projects.open=Abrir
 projects.close=Fechar
@@ -1356,6 +1362,7 @@ issues.choose.blank=Padrão
 issues.choose.blank_about=Criar uma issue a partir do modelo padrão.
 issues.choose.ignore_invalid_templates=Modelos inválidos foram ignorados
 issues.choose.invalid_templates=%v modelo(s) inválido(s) encontrado(s)
+issues.choose.invalid_config=A configuração da issue contém erros:
 issues.no_ref=Nenhum branch/tag especificado
 issues.create=Criar issue
 issues.new_label=Nova etiqueta
@@ -1426,7 +1433,6 @@ issues.filter_sort.moststars=Mais estrelas
 issues.filter_sort.feweststars=Menos estrelas
 issues.filter_sort.mostforks=Mais forks
 issues.filter_sort.fewestforks=Menos forks
-issues.keyword_search_unavailable=A pesquisa por palavra-chave não está disponível no momento. Entre em contato com o administrador do site.
 issues.action_open=Abrir
 issues.action_close=Fechar
 issues.action_label=Etiqueta
@@ -1479,6 +1485,11 @@ issues.author_helper=Este usuário é o autor.
 issues.role.owner=Proprietário
 issues.role.owner_helper=Este usuário é o dono deste repositório.
 issues.role.member=Membro
+issues.role.collaborator=Colaborador
+issues.role.collaborator_helper=Este usuário foi convidado para colaborar no repositório.
+issues.role.first_time_contributor=Primeira vez contribuindo
+issues.role.first_time_contributor_helper=Esta é a primeira contribuição deste usuário para o repositório.
+issues.role.contributor=Contribuidor
 issues.re_request_review=Re-solicitar revisão
 issues.is_stale=Houve alterações nessa PR desde essa revisão
 issues.remove_request_review=Remover solicitação de revisão
@@ -1494,6 +1505,8 @@ issues.label_description=Descrição da etiqueta
 issues.label_color=Cor da etiqueta
 issues.label_exclusive=Exclusivo
 issues.label_archive=Arquivar etiqueta
+issues.label_archived_filter=Mostrar etiquetas arquivadas
+issues.label_archive_tooltip=Etiquetas arquivadas são excluídas, por padrão, das sugestões ao pesquisar por etiqueta.
 issues.label_exclusive_desc=Nomeie o rótulo <code>escopo/item</code> para torná-lo mutuamente exclusivo com outros rótulos do <code>escopo/</code>.
 issues.label_exclusive_warning=Quaisquer rótulos com escopo conflitantes serão removidos ao editar os rótulos de uma issue ou pull request.
 issues.label_count=%d etiquetas
@@ -1572,6 +1585,7 @@ issues.due_date_form=dd/mm/aaaa
 issues.due_date_form_add=Adicionar data limite
 issues.due_date_form_edit=Editar
 issues.due_date_form_remove=Remover
+issues.due_date_not_writer=Você precisa de acesso de gravação a esse repositório para atualizar a data limite de uma issue.
 issues.due_date_not_set=Data limite não informada.
 issues.due_date_added=adicionou a data limite %s %s
 issues.due_date_modified=modificou a data limite de %[2]s para %[1]s %[3]s
@@ -1668,7 +1682,6 @@ pulls.compare_compare=pull de
 pulls.switch_comparison_type=Mudar tipo de comparação
 pulls.switch_head_and_base=Trocar cabeça e base
 pulls.filter_branch=Filtrar branch
-pulls.no_results=Nada encontrado.
 pulls.show_all_commits=Mostrar todos os commits
 pulls.show_changes_since_your_last_review=Mostrar alterações desde sua última revisão
 pulls.showing_only_single_commit=Mostrando apenas as alterações do commit %[1]s
@@ -1735,6 +1748,7 @@ pulls.merge_pull_request=Criar commit de merge
 pulls.rebase_merge_pull_request=Rebase e fast-forward
 pulls.rebase_merge_commit_pull_request=Rebase e criar commit de merge
 pulls.squash_merge_pull_request=Criar commit de squash
+pulls.fast_forward_only_merge_pull_request=Apenas Fast-forward
 pulls.merge_manually=Merge feito manualmente
 pulls.merge_commit_id=A ID de merge commit
 pulls.require_signed_wont_sign=O branch requer commits assinados, mas este merge não será assinado
@@ -1758,6 +1772,8 @@ pulls.status_checks_failure=Algumas verificações falharam
 pulls.status_checks_error=Algumas verificações reportaram erros
 pulls.status_checks_requested=Obrigatário
 pulls.status_checks_details=Detalhes
+pulls.status_checks_hide_all=Ocultar todas as verificações
+pulls.status_checks_show_all=Mostrar todas as verificações
 pulls.update_branch=Atualizar branch por merge
 pulls.update_branch_rebase=Atualizar branch por rebase
 pulls.update_branch_success=Atualização do branch foi bem-sucedida
@@ -1766,6 +1782,9 @@ pulls.outdated_with_base_branch=Este branch está desatualizado com o branch bas
 pulls.close=Fechar pull request
 pulls.closed_at=`fechou este pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 pulls.reopened_at=`reabriu este pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
+pulls.cmd_instruction_checkout_title=Checkout
+pulls.cmd_instruction_merge_title=Merge
+pulls.cmd_instruction_merge_desc=Faça merge das alterações e atualize no Gitea.
 pulls.clear_merge_message=Limpar mensagem do merge
 pulls.clear_merge_message_hint=Limpar a mensagem de merge só irá remover o conteúdo da mensagem de commit e manter trailers git gerados, como "Co-Authored-By …".
 
@@ -1862,6 +1881,8 @@ wiki.page_name_desc=Digite um nome para esta página Wiki. Alguns nomes especiai
 wiki.original_git_entry_tooltip=Ver o arquivo Git original em vez de usar o link amigável.
 
 activity=Atividade
+activity.navbar.pulse=Pulso
+activity.navbar.contributors=Contribuidores
 activity.period.filter_label=Período:
 activity.period.daily=1 dia
 activity.period.halfweekly=3 dias
@@ -1927,16 +1948,10 @@ activity.git_stats_and_deletions=e
 activity.git_stats_deletion_1=%d exclusão
 activity.git_stats_deletion_n=%d exclusões
 
-search=Pesquisar
-search.search_repo=Pesquisar no repositório...
-search.type.tooltip=Tipo de pesquisa
-search.fuzzy=Aproximada
-search.fuzzy.tooltip=Incluir resultados que sejam próximos ao termo de busca
-search.match=Corresponde
-search.match.tooltip=Incluir somente resultados que correspondam exatamente ao termo de busca
-search.results=Resultados da pesquisa para "%s" em <a href="%s">%s</a>
-search.code_no_results=Nenhum código-fonte correspondente ao seu termo de pesquisa foi encontrado.
-search.code_search_unavailable=A pesquisa por código não está disponível no momento. Entre em contato com o administrador do site.
+contributors.contribution_type.filter_label=Tipo de contribuição:
+contributors.contribution_type.commits=Commits
+contributors.contribution_type.additions=Adições
+contributors.contribution_type.deletions=Exclusões
 
 settings=Configurações
 settings.desc=Opções é onde você pode gerenciar as configurações para o repositório
@@ -2005,6 +2020,7 @@ settings.pulls.default_allow_edits_from_maintainers=Permitir edições de manten
 settings.releases_desc=Habilitar versões do Repositório
 settings.packages_desc=Habilitar Registro de Pacotes de Repositório
 settings.projects_desc=Habilitar Projetos do Repositório
+settings.projects_mode_all=Todos os projetos
 settings.actions_desc=Habilitar ações do repositório
 settings.admin_settings=Configurações do administrador
 settings.admin_enable_health_check=Habilitar verificações de integridade (git fsck) no repositório
@@ -2076,7 +2092,6 @@ settings.delete_collaborator=Remover
 settings.collaborator_deletion=Remover colaborador
 settings.collaborator_deletion_desc=A exclusão de um colaborador irá revogar o acesso a este repositório. Continuar?
 settings.remove_collaborator_success=O colaborador foi removido.
-settings.search_user_placeholder=Pesquisar usuário...
 settings.org_not_allowed_to_be_collaborator=Organizações não podem ser adicionadas como um colaborador.
 settings.change_team_access_not_allowed=Alteração do acesso da equipe para o repositório está restrito ao proprietário da organização
 settings.team_not_in_organization=A equipe não está na mesma organização que o repositório
@@ -2084,7 +2099,6 @@ settings.teams=Equipes
 settings.add_team=Adicionar Equipe
 settings.add_team_duplicate=A equipe já tem o repositório
 settings.add_team_success=A equipe agora tem acesso ao repositório.
-settings.search_team=Pesquisar Equipe…
 settings.change_team_permission_tip=A permissão da equipe está definida na página de configurações da equipe e não pode ser alterada por repositório
 settings.delete_team_tip=Esta equipe tem acesso a todos os repositórios e não pode ser removida
 settings.remove_team_success=O acesso da equipe ao repositório foi removido.
@@ -2229,9 +2243,7 @@ settings.protect_whitelist_committers=Lista permitida para push
 settings.protect_whitelist_committers_desc=Somente usuários ou equipes da lista permitida serão autorizados realizar push neste branch (mas não forçar o push).
 settings.protect_whitelist_deploy_keys=Dar permissão às chaves de deploy com acesso de gravação para push.
 settings.protect_whitelist_users=Usuários com permissão para realizar push:
-settings.protect_whitelist_search_users=Pesquisar usuários...
 settings.protect_whitelist_teams=Equipes com permissão para realizar push:
-settings.protect_whitelist_search_teams=Pesquisar equipes...
 settings.protect_merge_whitelist_committers=Habilitar controle de permissão de merge
 settings.protect_merge_whitelist_committers_desc=Permitir que determinados usuários ou equipes possam aplicar merge de pull requests neste branch.
 settings.protect_merge_whitelist_users=Usuários com permissão para aplicar merge:
@@ -2298,6 +2310,9 @@ settings.archive.error=Um erro ocorreu enquanto estava sendo arquivado o reposit
 settings.archive.error_ismirror=Você não pode arquivar um repositório espelhado.
 settings.archive.branchsettings_unavailable=Configurações do branch não estão disponíveis quando o repositório está arquivado.
 settings.archive.tagsettings_unavailable=As configurações de tag não estão disponíveis se o repositório estiver arquivado.
+settings.unarchive.button=Desarquivar o repositório
+settings.unarchive.header=Desarquivar este repositório
+settings.unarchive.success=O repositório foi desarquivado com sucesso.
 settings.update_avatar_success=O avatar do repositório foi atualizado.
 settings.lfs=LFS
 settings.lfs_filelist=Arquivos LFS armazenados neste repositório
@@ -2420,6 +2435,7 @@ release.edit_release=Atualizar versão
 release.delete_release=Excluir versão
 release.delete_tag=Apagar Tag
 release.deletion=Excluir versão
+release.deletion_desc=A exclusão de uma versão apenas a remove do Gitea. Isso não afetará a tag do Git, o conteúdo do seu repositório ou seu histórico. Continuar?
 release.deletion_success=A versão foi excluída.
 release.deletion_tag_desc=A tag será excluída do repositório. Conteúdo do repositório e histórico permanecerão inalterados. Continuar?
 release.deletion_tag_success=A tag foi excluída.
@@ -2484,6 +2500,11 @@ error.csv.too_large=Não é possível renderizar este arquivo porque ele é muit
 error.csv.unexpected=Não é possível renderizar este arquivo porque ele contém um caractere inesperado na linha %d e coluna %d.
 error.csv.invalid_field_count=Não é possível renderizar este arquivo porque ele tem um número errado de campos na linha %d.
 
+[graphs]
+component_loading=Carregando %s...
+component_loading_failed=Não foi possível carregar %s
+component_loading_info=Isto pode demorar um pouco…
+
 [org]
 org_name_holder=Nome da organização
 org_full_name_holder=Nome completo da organização
@@ -2513,6 +2534,7 @@ form.create_org_not_allowed=Você não tem permissão para criar uma organizaç
 settings=Configurações
 settings.options=Organização
 settings.full_name=Nome completo
+settings.email=E-mail de contato
 settings.website=Site
 settings.location=Localização
 settings.permission=Permissões
@@ -2585,7 +2607,6 @@ teams.write_permission_desc=Esta equipe concede acesso para <strong>escrita</str
 teams.admin_permission_desc=Esta equipe concede acesso de <strong>Administrador</strong>: Membros podem ler, fazer push e adicionar outros colaboradores para os repositórios da equipe.
 teams.create_repo_permission_desc=Além disso, esta equipe concede permissão de <strong>Criar repositório</strong>: membros podem criar novos repositórios na organização.
 teams.repositories=Repositórios da equipe
-teams.search_repo_placeholder=Pesquisar repositório...
 teams.remove_all_repos_title=Remover todos os repositórios da equipe
 teams.remove_all_repos_desc=Isto irá remover todos os repositórios da equipe.
 teams.add_all_repos_title=Adicionar todos os repositórios
@@ -2601,11 +2622,13 @@ teams.all_repositories_helper=A equipe tem acesso a todos os repositórios. Sele
 teams.all_repositories_read_permission_desc=Esta equipe concede acesso <strong>Leitura</strong> a <strong>todos os repositórios</strong>: membros podem ver e clonar repositórios.
 teams.all_repositories_write_permission_desc=Esta equipe concede acesso <strong>Escrita</strong> a <strong>todos os repositórios</strong>: os membros podem ler de e fazer push para os repositórios.
 teams.all_repositories_admin_permission_desc=Esta equipe concede acesso <strong>Administrativo</strong> a <strong>todos os repositórios</strong>: os membros podem ler, fazer push e adicionar colaboradores aos repositórios.
+teams.invite.title=Você foi convidado para fazer parte da equipe <strong>%s</strong> na organização <strong>%s</strong>.
 teams.invite.by=Convidado por %s
 teams.invite.description=Por favor, clique no botão abaixo para se juntar à equipe.
 
 [admin]
 dashboard=Painel
+identity_access=Identidade e acesso
 users=Contas de usuário
 organizations=Organizações
 repositories=Repositórios
@@ -2614,15 +2637,17 @@ integrations=Integrações
 authentication=Fontes de autenticação
 emails=E-mails do Usuário
 config=Configuração
+config_summary=Resumo
+config_settings=Configurações
 notices=Avisos do sistema
 monitor=Monitoramento
 first_page=Primeira
 last_page=Última
 total=Total: %d
+settings=Configurações de Administrador
 
 dashboard.new_version_hint=Uma nova versão está disponível: %s. Versão atual: %s. Visite <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">o blog</a> para mais informações.
 dashboard.statistic=Resumo
-dashboard.operations=Operações de manutenção
 dashboard.system_status=Status do sistema
 dashboard.operation_name=Nome da operação
 dashboard.operation_switch=Trocar
@@ -2777,9 +2802,6 @@ repos.unadopted.no_more=Não foram encontrados mais repositórios não adotados
 repos.owner=Proprietário
 repos.name=Nome
 repos.private=Privado
-repos.watches=Observadores
-repos.stars=Favoritos
-repos.forks=Forks
 repos.issues=Issues
 repos.size=Tamanho
 repos.lfs_size=Tamanho do LFS
@@ -2901,7 +2923,6 @@ auths.tip.nextcloud=`Registre um novo consumidor OAuth em sua instância usando
 auths.tip.dropbox=Criar um novo aplicativo em https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Cadastrar um novo aplicativo em https://developers.facebook.com/apps e adicionar o produto "Facebook Login"`
 auths.tip.github=Cadastrar um novo aplicativo de OAuth na https://github.com/settings/applications/new
-auths.tip.gitlab=Cadastrar um novo aplicativo em https://gitlab.com/profile/applications
 auths.tip.google_plus=Obter credenciais de cliente OAuth2 do console de API do Google em https://console.developers.google.com/
 auths.tip.openid_connect=Use o OpenID Connect Discovery URL (<servidor>/.well-known/openid-configuration) para especificar os endpoints
 auths.tip.twitter=Vá em https://dev.twitter.com/apps, crie um aplicativo e certifique-se de que está habilitada a opção “Allow this application to be used to Sign in with Twitter“
@@ -3110,6 +3131,7 @@ notices.desc=Descrição
 notices.op=Op.
 notices.delete_success=Os avisos do sistema foram excluídos.
 
+
 [action]
 create_repo=criou o repositório <a href="%s">%s</a>
 rename_repo=renomeou o repositório <code>%[1]s</code> para <a href="%[2]s">%[3]s</a>
@@ -3293,6 +3315,8 @@ rpm.registry=Configure este registro pela linha de comando:
 rpm.distros.redhat=em distribuições baseadas no RedHat
 rpm.distros.suse=em distribuições baseadas no SUSE
 rpm.install=Para instalar o pacote, execute o seguinte comando:
+rpm.repository=Informações do repositório
+rpm.repository.architectures=Arquiteturas
 rubygems.install=Para instalar o pacote usando gem, execute o seguinte comando:
 rubygems.install2=ou adicione-o ao Gemfile:
 rubygems.dependencies.runtime=Dependências de Execução
@@ -3406,13 +3430,20 @@ runners.status.idle=Inativo
 runners.status.active=Ativo
 runners.status.offline=Offiline
 runners.version=Versão
+runners.reset_registration_token=Redefinir token de registro
 runners.reset_registration_token_success=Token de registro de runner redefinido com sucesso
 
 runs.all_workflows=Todos os Workflows
 runs.commit=Commit
+runs.scheduled=Agendado
 runs.pushed_by=push feito por
 runs.invalid_workflow_helper=O arquivo de configuração do workflow é inválido. Por favor, verifique seu arquivo de configuração: %s
+runs.actor=Ator
 runs.status=Status
+runs.actors_no_select=Todos os atores
+runs.status_no_select=Todos os Status
+runs.no_results=Não houve correspondência de resultados.
+runs.empty_commit_message=(mensagem de commit vazia)
 
 
 need_approval_desc=Precisa de aprovação para executar workflows para pull request do fork.
@@ -3425,5 +3456,9 @@ type-3.display_name=Projeto da organização
 
 [git.filemode]
 ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Diretório
+normal_file=Arquivo normal
+executable_file=Arquivo executável
 symbolic_link=Link simbólico
+submodule=Submódulo
 
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 863a1545c3..59b3d3df67 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -25,6 +25,7 @@ enable_javascript=Este sítio Web requer JavaScript.
 toc=Índice
 licenses=Licenças
 return_to_gitea=Retornar ao Gitea
+more_items=Mais itens
 
 username=Nome de utilizador
 email=Endereço de email
@@ -113,6 +114,7 @@ loading=Carregando…
 error=Erro
 error404=A página que pretende aceder <strong>não existe</strong> ou <strong>não tem autorização</strong> para a ver.
 go_back=Voltar
+invalid_data=Dados inválidos: %v
 
 never=Nunca
 unknown=Desconhecido
@@ -123,6 +125,7 @@ pin=Fixar
 unpin=Desafixar
 
 artifacts=Artefactos
+confirm_delete_artifact=Tem a certeza que quer eliminar este artefacto "%s"?
 
 archived=Arquivado
 
@@ -141,6 +144,43 @@ confirm_delete_selected=Confirma a exclusão de todos os itens marcados?
 name=Nome
 value=Valor
 
+filter=Filtro
+filter.clear=Retirar filtro
+filter.is_archived=Arquivado
+filter.not_archived=Não arquivado
+filter.is_fork=Derivado
+filter.not_fork=Não derivado
+filter.is_mirror=Replicado
+filter.not_mirror=Não replicado
+filter.is_template=Modelo
+filter.not_template=Não é modelo
+filter.public=Público
+filter.private=Privado
+
+no_results_found=Não foram encontrados quaisquer resultados.
+
+[search]
+search=Pesquisar...
+type_tooltip=Tipo de pesquisa
+fuzzy=Aproximada
+fuzzy_tooltip=Incluir também os resultados que estejam próximos do termo de pesquisa
+match=Fiel
+match_tooltip=Incluir somente os resultados que correspondam rigorosamente ao termo de pesquisa
+repo_kind=Pesquisar repositórios...
+user_kind=Pesquisar utilizadores...
+org_kind=Pesquisar organizações...
+team_kind=Pesquisar equipas...
+code_kind=Pesquisar código...
+code_search_unavailable=A pesquisa de código não está disponível, neste momento. Entre em contacto com o administrador.
+code_search_by_git_grep=Os resultados da pesquisa no código-fonte neste momento são fornecidos pelo "git grep". Esses resultados podem ser melhores se o administrador habilitar o indexador do repositório.
+package_kind=Pesquisar pacotes...
+project_kind=Pesquisar planeamentos...
+branch_kind=Pesquisar ramos...
+commit_kind=Pesquisar cometimentos...
+runner_kind=Pesquisar executores...
+no_results=Não foram encontrados resultados correspondentes.
+keyword_search_unavailable=Pesquisar por palavra-chave não está disponível, neste momento. Entre em contacto com o administrador.
+
 [aria]
 navbar=Barra de navegação
 footer=Rodapé
@@ -246,6 +286,7 @@ email_title=Configurações de email
 smtp_addr=Servidor SMTP
 smtp_port=Porto do SMTP
 smtp_from=Email do remetente
+smtp_from_invalid=O endereço para "Enviar email como" é inválido
 smtp_from_helper=Endereço de email que o Gitea vai usar. Insira um endereço de email simples ou use o formato "Nome" <email@exemplo.com>.
 mailer_user=Nome de utilizador do SMTP
 mailer_password=Senha do SMTP
@@ -305,6 +346,7 @@ env_config_keys=Configuração do ambiente
 env_config_keys_prompt=As seguintes variáveis de ambiente também serão aplicadas ao seu ficheiro de configuração:
 
 [home]
+nav_menu=Menu de navegação
 uname_holder=Nome de utilizador ou endereço de email
 password_holder=Senha
 switch_dashboard_context=Trocar contexto do painel
@@ -314,7 +356,6 @@ collaborative_repos=Repositórios colaborativos
 my_orgs=As minhas organizações
 my_mirrors=As minhas réplicas
 view_home=Ver %s
-search_repos=Procurar um repositório…
 filter=Outros filtros
 filter_by_team_repositories=Filtrar por repositórios da equipa
 feed_of=`Fonte de "%s"`
@@ -335,20 +376,8 @@ issues.in_your_repos=Nos seus repositórios
 repos=Repositórios
 users=Utilizadores
 organizations=Organizações
-search=Procurar
 go_to=Ir para
 code=Código
-search.type.tooltip=Tipo de pesquisa
-search.fuzzy=Aproximada
-search.fuzzy.tooltip=Incluir também os resultados que estejam próximos do termo de pesquisa
-search.match=Fiel
-search.match.tooltip=Incluir somente os resultados que correspondam rigorosamente ao termo de pesquisa
-code_search_unavailable=A pesquisa por código-fonte não está disponível, neste momento. Entre em contacto com o administrador.
-repo_no_results=Não foram encontrados quaisquer repositórios correspondentes.
-user_no_results=Não foram encontrados quaisquer utilizadores correspondentes.
-org_no_results=Não foram encontradas quaisquer organizações correspondentes.
-code_no_results=Não foi encontrado qualquer código-fonte correspondente à sua pesquisa.
-code_search_results=`Resultados da pesquisa para "%s"`
 code_last_indexed_at=Última indexação %s
 relevant_repositories_tooltip=Repositórios que são derivações ou que não têm tópico, nem ícone, nem descrição, estão escondidos.
 relevant_repositories=Apenas estão a ser mostrados os repositórios relevantes. <a href="%s">Mostrar resultados não filtrados</a>.
@@ -366,7 +395,7 @@ forgot_password_title=Esqueci-me da senha
 forgot_password=Esqueceu a sua senha?
 sign_up_now=Precisa de uma conta? Inscreva-se agora.
 sign_up_successful=A conta foi criada com sucesso. Bem-vindo/a!
-confirmation_mail_sent_prompt=Foi enviado um novo email de confirmação para <b>%s</b>. Verifique a sua caixa de entrada dentro de %s para completar o processo de inscrição.
+confirmation_mail_sent_prompt_ex=Foi enviado um email de confirmação para <b>%s</b>. Verifique a sua caixa de entrada dentro de %s para completar o processo de registo. Se o seu endereço de email de registo estiver errado, pode iniciar a sessão novamente e mudá-lo.
 must_change_password=Mude a sua senha
 allow_password_change=Exigir que o utilizador mude a senha (recomendado)
 reset_password_mail_sent_prompt=Foi enviado um email de confirmação para <b>%s</b>. Verifique a sua caixa de entrada dentro de %s para completar o processo de recuperação.
@@ -376,6 +405,7 @@ prohibit_login=Início de sessão proibido
 prohibit_login_desc=A sua conta está proibida de iniciar sessão. Contacte o administrador.
 resent_limit_prompt=Já fez um pedido recentemente para enviar um email para pôr a conta em funcionamento. Espere 3 minutos e tente novamente.
 has_unconfirmed_mail=Olá %s, tem um endereço de email não confirmado (<b>%s</b>). Se não recebeu um email de confirmação ou precisa de o voltar a enviar, clique no botão abaixo.
+change_unconfirmed_mail_address=Se o seu endereço de email estiver errado, pode mudá-lo aqui e enviar um novo email de confirmação.
 resend_mail=Clique aqui para voltar a enviar um email para pôr a conta em funcionamento
 email_not_associate=O endereço de email não está associado a qualquer conta.
 send_reset_mail=Enviar email de recuperação da conta
@@ -423,6 +453,7 @@ authorization_failed_desc=A autorização falhou porque encontrámos um pedido i
 sspi_auth_failed=Falhou a autenticação SSPI
 password_pwned=A senha utilizada está numa <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">lista de senhas roubadas</a> anteriormente expostas em fugas de dados públicas. Tente novamente com uma senha diferente e considere também mudar esta senha nos outros sítios.
 password_pwned_err=Não foi possível completar o pedido ao HaveIBeenPwned
+last_admin=Não pode remover o último administrador. Tem que existir pelo menos um administrador.
 
 [mail]
 view_it_on=Ver em %s
@@ -555,6 +586,7 @@ team_name_been_taken=O nome da equipa já foi tomado.
 team_no_units_error=Permitir acesso a pelo menos uma secção do repositório.
 email_been_used=O endereço de email já está em uso.
 email_invalid=O endereço de email é inválido.
+email_domain_is_not_allowed=O domínio do email de utilizador <b>%s</b> entra en conflito com o EMAIL_DOMAIN_ALLOWLIST ou com o EMAIL_DOMAIN_BLOCKLIST. Verifique se a operação estava prevista.
 openid_been_used=O endereço OpenID "%s" já está em uso.
 username_password_incorrect=O nome de utilizador ou a senha estão errados.
 password_complexity=A senha não passa nos requisitos de complexidade:
@@ -566,6 +598,8 @@ enterred_invalid_repo_name=O nome do repositório que inseriu está errado.
 enterred_invalid_org_name=O nome da organização que inseriu está errado.
 enterred_invalid_owner_name=O novo nome de proprietário não é válido.
 enterred_invalid_password=A senha que inseriu está errada.
+unset_password=O utilizador não definiu a senha.
+unsupported_login_type=O tipo de início de sessão não é suportado para eliminar a conta.
 user_not_exist=O utilizador não existe.
 team_not_exist=A equipa não existe.
 last_org_owner=Não pode remover o último utilizador da equipa 'proprietários'. Tem que haver pelo menos um proprietário numa organização.
@@ -588,6 +622,8 @@ org_still_own_packages=Esta organização ainda possui um ou mais pacotes, elimi
 
 target_branch_not_exist=O ramo de destino não existe.
 
+admin_cannot_delete_self=Não se pode auto-remover quando tem privilégios de administração. Remova esses privilégios primeiro.
+
 [user]
 change_avatar=Mude o seu avatar…
 joined_on=Inscreveu-se em %s
@@ -613,6 +649,30 @@ form.name_reserved=O nome de utilizador "%s" está reservado.
 form.name_pattern_not_allowed=O padrão "%s" não é permitido no nome de utilizador.
 form.name_chars_not_allowed=O nome de utilizador "%s" contém caracteres inválidos.
 
+block.block=Bloquear
+block.block.user=Bloquear utilizador
+block.block.org=Bloquear utilizador para a organização
+block.block.failure=Falhou o bloqueio do utilizador: %s
+block.unblock=Desbloquear
+block.unblock.failure=Falhou o desbloqueio do utilizador: %s
+block.blocked=Bloqueou este utilizador.
+block.title=Bloquear um utilizador
+block.info=Bloquear um utilizador evita que este interaja com repositórios, tal como abrir ou comentar em pedidos de integração ou questões. Saiba mais sobre como bloquear um utilizador.
+block.info_1=Bloquear um utilizador impede as seguintes operações na sua conta e nos seus repositórios:
+block.info_2=seguir a sua conta
+block.info_3=enviar-lhe notificações ao @mencionar o seu nome de utilizador
+block.info_4=convidá-lo/a para ser colaborador/a nos repositórios dele/dela
+block.info_5=juntar aos favoritos, derivar ou vigiar repositórios
+block.info_6=abrir e comentar questões ou pedidos de integração
+block.info_7=reagir aos seus comentários em questões ou pedidos de integração
+block.user_to_block=Utilizador a bloquear
+block.note=Nota
+block.note.title=Nota opcional:
+block.note.info=A nota não é visível para o utilizador bloqueado.
+block.note.edit=Editar nota
+block.list=Utilizadores bloqueados
+block.list.none=Você ainda não bloqueou quaisquer utilizadores.
+
 [settings]
 profile=Perfil
 account=Conta
@@ -757,7 +817,6 @@ gpg_invalid_token_signature=A chave GPG, assinatura ou código fornecidos não c
 gpg_token_required=Tem que fornecer uma assinatura para o código abaixo
 gpg_token=Código
 gpg_token_help=Pode gerar uma assinatura usando o seguinte comando:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Assinatura GPG blindada (com armadura ASCII)
 key_signature_gpg_placeholder=Começa com '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=A chave GPG "%s" foi validada.
@@ -950,8 +1009,9 @@ fork_visibility_helper=A visibilidade de um repositório derivado não poderá s
 fork_branch=Ramo a ser clonado para a derivação
 all_branches=Todos os ramos
 fork_no_valid_owners=Não pode fazer uma derivação deste repositório porque não existem proprietários válidos.
+fork.blocked_user=Não pode derivar o repositório porque foi bloqueado/a pelo/a proprietário/a do repositório.
 use_template=Usar este modelo
-clone_in_vsc=Clonar no VS Code
+open_with_editor=Abrir com %s
 download_zip=Descarregar ZIP
 download_tar=Descarregar TAR.GZ
 download_bundle=Descarregar PACOTE
@@ -967,6 +1027,8 @@ issue_labels_helper=Escolha um conjunto de rótulos para as questões.
 license=Licença
 license_helper=Escolha um ficheiro de licença.
 license_helper_desc=Uma licença rege o que os outros podem, ou não, fazer com o seu código fonte. Não tem a certeza sobre qual a mais indicada para o seu trabalho? Veja: <a target="_blank" rel="noopener noreferrer" href="%s">Escolher uma licença.</a>
+object_format=Formato dos elementos
+object_format_helper=Formato dos elementos do repositório. Não poderá ser alterado mais tarde. SHA1 é o mais compatível.
 readme=README
 readme_helper=Escolha um modelo de ficheiro README.
 readme_helper_desc=Este é o sítio onde pode escrever uma descrição completa do seu trabalho.
@@ -984,6 +1046,7 @@ mirror_prune=Podar
 mirror_prune_desc=Remover referências obsoletas de seguimento remoto
 mirror_interval=Intervalo entre sincronizações (as unidades de tempo válidas são 'h', 'm' e 's'). O valor zero desabilita a sincronização periódica. (Intervalo mínimo: %s)
 mirror_interval_invalid=O intervalo entre sincronizações não é válido.
+mirror_sync=sincronizado
 mirror_sync_on_commit=Sincronizar quando forem enviados cometimentos
 mirror_address=Clonar a partir do URL
 mirror_address_desc=Coloque, na secção de autorização, as credenciais que, eventualmente, sejam necessárias.
@@ -1001,6 +1064,7 @@ watchers=Vigilantes
 stargazers=Fãs
 stars_remove_warning=Isto irá remover todas as marcas de favoritos deste repositório.
 forks=Derivações
+stars=Favoritos
 reactions_more=e mais %d
 unit_disabled=O administrador desabilitou esta secção do repositório.
 language_other=Outros
@@ -1034,6 +1098,7 @@ desc.public=Público
 desc.template=Modelo
 desc.internal=Interno
 desc.archived=Arquivado
+desc.sha256=SHA256
 
 template.items=Itens do modelo
 template.git_content=Conteúdo Git (ramo principal)
@@ -1121,6 +1186,7 @@ watch=Vigiar
 unstar=Tirar dos favoritos
 star=Juntar aos favoritos
 fork=Derivar
+action.blocked_user=Não pode realizar a operação porque foi bloqueado/a pelo/a proprietário/a do repositório.
 download_archive=Descarregar repositório
 more_operations=Mais operações
 
@@ -1167,6 +1233,8 @@ file_view_rendered=Ver resultado processado
 file_view_raw=Ver em bruto
 file_permalink=Ligação permanente
 file_too_large=O ficheiro é demasiado grande para ser apresentado.
+code_preview_line_from_to=Linhas %[1]d até %[2]d em %[3]s
+code_preview_line_in=Linha %[1]d em %[2]s
 invisible_runes_header=`Este ficheiro contém caracteres Unicode invisíveis`
 invisible_runes_description=`Este ficheiro contém caracteres Unicode indistinguíveis para humanos mas que podem ser processados de forma diferente por um computador. Se acha que é intencional, pode ignorar este aviso com segurança. Use o botão Revelar para os mostrar.`
 ambiguous_runes_header=`Este ficheiro contém caracteres Unicode ambíguos`
@@ -1184,6 +1252,8 @@ audio_not_supported_in_browser=O seu navegador não suporta a etiqueta 'audio' d
 stored_lfs=Armazenado com Git LFS
 symbolic_link=Ligação simbólica
 executable_file=Ficheiro executável
+vendored=Externo
+generated=Gerado
 commit_graph=Gráfico de cometimentos
 commit_graph.select=Escolher ramos
 commit_graph.hide_pr_refs=Ocultar pedidos de integração
@@ -1247,6 +1317,8 @@ editor.file_editing_no_longer_exists=O ficheiro que está a ser editado, "%s", j
 editor.file_deleting_no_longer_exists=O ficheiro que está a ser eliminado, "%s", já não existe neste repositório.
 editor.file_changed_while_editing=O conteúdo do ficheiro mudou desde que começou a editar. <a target="_blank" rel="noopener noreferrer" href="%s">Clique aqui</a> para ver as modificações ou clique em <strong>Cometer novamente</strong> para escrever por cima.
 editor.file_already_exists=Já existe um ficheiro com o nome "%s" neste repositório.
+editor.commit_id_not_matching=O ID do cometimento não corresponde ao ID de quando começou a editar. Faça o cometimento para um ramo de remendo (patch) e depois faça a integração.
+editor.push_out_of_date=O envio parece estar obsoleto.
 editor.commit_empty_file_header=Cometer um ficheiro vazio
 editor.commit_empty_file_text=O ficheiro que está prestes a cometer está vazio. Quer continuar?
 editor.no_changes_to_show=Não existem modificações para mostrar.
@@ -1270,9 +1342,8 @@ commits.desc=Navegar pelo histórico de modificações no código fonte.
 commits.commits=Cometimentos
 commits.no_commits=Não há cometimentos em comum. "%s" e "%s" têm históricos completamente diferentes.
 commits.nothing_to_compare=Estes ramos são iguais.
-commits.search=Procurar cometimentos…
 commits.search.tooltip=Pode prefixar palavras-chave com "author:", "committer:", "after:", ou "before:". Por exemplo: "revert author:Alice before:2019-01-13".
-commits.find=Procurar
+commits.search_branch=Este ramo
 commits.search_all=Todos os ramos
 commits.author=Autor(a)
 commits.message=Mensagem
@@ -1323,7 +1394,6 @@ projects.type.basic_kanban=Kanban básico
 projects.type.bug_triage=Triagem de erros
 projects.template.desc=Modelo de planeamento
 projects.template.desc_helper=Escolha um modelo de planeamento para começar
-projects.type.uncategorized=Sem categoria
 projects.column.edit=Editar coluna
 projects.column.edit_title=Nome
 projects.column.new_title=Nome
@@ -1331,8 +1401,6 @@ projects.column.new_submit=Criar coluna
 projects.column.new=Nova coluna
 projects.column.set_default=Tornar predefinida
 projects.column.set_default_desc=Definir esta coluna como a predefinida para questões e pedidos de integração não categorizados
-projects.column.unset_default=Deixar de ser a predefinida
-projects.column.unset_default_desc=Faz com que esta coluna deixe de ser a predefinida
 projects.column.delete=Eliminar coluna
 projects.column.deletion_desc=Eliminar uma coluna de um planeamento faz com que todas as questões que nela constam sejam movidas para a coluna 'Sem categoria'. Continuar?
 projects.column.color=Colorido
@@ -1369,6 +1437,8 @@ issues.new.assignees=Encarregados
 issues.new.clear_assignees=Retirar todos os encarregados
 issues.new.no_assignees=Sem encarregados
 issues.new.no_reviewers=Sem revisores
+issues.new.blocked_user=Não pode criar a questão porque foi bloqueado/a pelo/a proprietário/a do repositório.
+issues.edit.blocked_user=Não pode editar o conteúdo porque foi bloqueado/a pelo/a remetente ou pelo/a proprietário/a do repositório.
 issues.choose.get_started=Começar
 issues.choose.open_external_link=Abrir
 issues.choose.blank=Padrão
@@ -1446,7 +1516,6 @@ issues.filter_sort.moststars=Favorito (decrescente)
 issues.filter_sort.feweststars=Favorito (crescente)
 issues.filter_sort.mostforks=Mais derivações
 issues.filter_sort.fewestforks=Menos derivações
-issues.keyword_search_unavailable=A pesquisa por palavra-chave não está disponível, neste momento. Entre em contacto com o administrador.
 issues.action_open=Abrir
 issues.action_close=Fechar
 issues.action_label=Rótulo
@@ -1484,6 +1553,7 @@ issues.close_comment_issue=Comentar e fechar
 issues.reopen_issue=Reabrir
 issues.reopen_comment_issue=Comentar e reabrir
 issues.create_comment=Comentar
+issues.comment.blocked_user=Não pode criar ou editar o comentário porque foi bloqueado/a pelo remetente ou pelo/a proprietário/a do repositório.
 issues.closed_at=`encerrou esta questão <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`reabriu esta questão <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.commit_ref_at=`referenciou esta questão num cometimento <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@@ -1682,6 +1752,7 @@ compare.compare_head=comparar
 
 pulls.desc=Habilitar pedidos de integração e revisão de código.
 pulls.new=Novo pedido de integração
+pulls.new.blocked_user=Não pode criar o pedido de integração porque foi bloqueado/a pelo/a proprietário/a do repositório.
 pulls.view=Ver pedido de integração
 pulls.compare_changes=Novo pedido de integração
 pulls.allow_edits_from_maintainers=Permitir edições por parte dos responsáveis
@@ -1698,7 +1769,6 @@ pulls.compare_compare=puxar de
 pulls.switch_comparison_type=Trocar o tipo de comparação
 pulls.switch_head_and_base=Trocar o topo com a base
 pulls.filter_branch=Filtrar ramo
-pulls.no_results=Não foram encontrados quaisquer resultados.
 pulls.show_all_commits=Mostrar todos os cometimentos
 pulls.show_changes_since_your_last_review=Mostrar modificações desde a sua última revisão
 pulls.showing_only_single_commit=Mostrando apenas as modificações do comentimento %[1]s
@@ -1707,6 +1777,7 @@ pulls.select_commit_hold_shift_for_range=Escolha o comentimento. Mantenha premid
 pulls.review_only_possible_for_full_diff=A revisão só é possível ao visualizar o diff completo
 pulls.filter_changes_by_commit=Filtrar por cometimento
 pulls.nothing_to_compare=Estes ramos são iguais. Não há necessidade de criar um pedido de integração.
+pulls.nothing_to_compare_have_tag=O ramo/etiqueta escolhidos são iguais.
 pulls.nothing_to_compare_and_allow_empty_pr=Estes ramos são iguais. Este pedido de integração ficará vazio.
 pulls.has_pull_request=`Já existe um pedido de integração entre estes ramos: <a href="%[1]s">%[2]s#%[3]d</a>`
 pulls.create=Criar um pedido de integração
@@ -1765,6 +1836,7 @@ pulls.merge_pull_request=Criar um cometimento de integração
 pulls.rebase_merge_pull_request=Mudar a base e avançar rapidamente
 pulls.rebase_merge_commit_pull_request=Mudar a base e criar um cometimento de integração
 pulls.squash_merge_pull_request=Criar cometimento de compactação
+pulls.fast_forward_only_merge_pull_request=Avançar rapidamente apenas
 pulls.merge_manually=Integrado manualmente
 pulls.merge_commit_id=O ID de cometimento da integração
 pulls.require_signed_wont_sign=O ramo requer que os cometimentos sejam assinados mas esta integração não vai ser assinada
@@ -1901,6 +1973,10 @@ wiki.page_name_desc=Insira um nome para esta página Wiki. Alguns dos nomes espe
 wiki.original_git_entry_tooltip=Ver o ficheiro Git original, ao invés de usar uma ligação amigável.
 
 activity=Trabalho
+activity.navbar.pulse=Pulso
+activity.navbar.code_frequency=Frequência de programação
+activity.navbar.contributors=Contribuidores
+activity.navbar.recent_commits=Cometimentos recentes
 activity.period.filter_label=Período:
 activity.period.daily=1 dia
 activity.period.halfweekly=3 dias
@@ -1966,16 +2042,10 @@ activity.git_stats_and_deletions=e
 activity.git_stats_deletion_1=%d eliminação
 activity.git_stats_deletion_n=%d eliminações
 
-search=Procurar
-search.search_repo=Procurar repositório
-search.type.tooltip=Tipo de pesquisa
-search.fuzzy=Aproximada
-search.fuzzy.tooltip=Incluir também os resultados que estejam próximos do termo de pesquisa
-search.match=Fiel
-search.match.tooltip=Incluir somente os resultados que correspondam rigorosamente ao termo de pesquisa
-search.results=Resultados da procura de "%s" em <a href="%s">%s</a>
-search.code_no_results=Não foi encontrado qualquer código-fonte correspondente à sua pesquisa.
-search.code_search_unavailable=A pesquisa por código-fonte não está disponível, neste momento. Entre em contacto com o administrador.
+contributors.contribution_type.filter_label=Tipo de contribuição:
+contributors.contribution_type.commits=Cometimentos
+contributors.contribution_type.additions=Adições
+contributors.contribution_type.deletions=Eliminações
 
 settings=Configurações
 settings.desc=Configurações é onde pode gerir as configurações do repositório
@@ -2003,6 +2073,7 @@ settings.mirror_settings.docs.doc_link_title=Como é que eu replico repositório
 settings.mirror_settings.docs.doc_link_pull_section=a parte "Puxar de um repositório remoto" da documentação.
 settings.mirror_settings.docs.pulling_remote_title=Puxando a partir de um repositório remoto
 settings.mirror_settings.mirrored_repository=Repositório replicado
+settings.mirror_settings.pushed_repository=Repositório enviado
 settings.mirror_settings.direction=Sentido
 settings.mirror_settings.direction.pull=Puxada
 settings.mirror_settings.direction.push=Envio
@@ -2024,6 +2095,8 @@ settings.branches.add_new_rule=Adicionar nova regra
 settings.advanced_settings=Configurações avançadas
 settings.wiki_desc=Habilitar wiki do repositório
 settings.use_internal_wiki=Usar o wiki nativo
+settings.default_wiki_branch_name=Nome do ramo predefinido do wiki
+settings.failed_to_change_default_wiki_branch=Falhou ao mudar o nome do ramo predefinido do wiki.
 settings.use_external_wiki=Usar um wiki externo
 settings.external_wiki_url=URL do wiki externo
 settings.external_wiki_url_error=O URL do wiki externo não é um URL válido.
@@ -2054,6 +2127,10 @@ settings.pulls.default_allow_edits_from_maintainers=Permitir, por norma, que os
 settings.releases_desc=Habilitar lançamentos no repositório
 settings.packages_desc=Habilitar o registo de pacotes do repositório
 settings.projects_desc=Habilitar planeamentos no repositório
+settings.projects_mode_desc=Modo de planeamentos (tipos de planeamentos a mostrar)
+settings.projects_mode_repo=Apenas planeamentos de repositórios
+settings.projects_mode_owner=Apenas planeamentos de utilizadores ou de organizações
+settings.projects_mode_all=Todos os planeamentos
 settings.actions_desc=Habilitar operações no repositório (Gitea Actions)
 settings.admin_settings=Configurações do administrador
 settings.admin_enable_health_check=Habilitar verificações de integridade (git fsck) no repositório
@@ -2079,6 +2156,7 @@ settings.convert_fork_succeed=A derivação foi convertida num repositório norm
 settings.transfer=Transferir a propriedade
 settings.transfer.rejected=A transferência do repositório foi rejeitada.
 settings.transfer.success=A transferência do repositório foi bem sucedida.
+settings.transfer.blocked_user=Não foi possível transferir o repositório porque foi bloqueado/a pelo/a novo/a proprietário/a.
 settings.transfer_abort=Cancelar a transferência
 settings.transfer_abort_invalid=Não pode cancelar a transferência de um repositório inexistente.
 settings.transfer_abort_success=A transferência de repositório para %s foi cancelada com sucesso.
@@ -2124,11 +2202,11 @@ settings.add_collaborator_success=O colaborador foi adicionado.
 settings.add_collaborator_inactive_user=Não é possível adicionar um utilizador desabilitado como colaborador.
 settings.add_collaborator_owner=Não é possível adicionar um proprietário como um colaborador.
 settings.add_collaborator_duplicate=O colaborador já tinha sido adicionado a este repositório.
+settings.add_collaborator.blocked_user=O/A colaborador/a foi bloqueado/a pelo/a proprietário/a do repositório ou vice-versa.
 settings.delete_collaborator=Remover
 settings.collaborator_deletion=Remover colaborador
 settings.collaborator_deletion_desc=Remover um colaborador irá revogar o seu acesso a este repositório. Quer continuar?
 settings.remove_collaborator_success=O colaborador foi removido.
-settings.search_user_placeholder=Procurar utilizador…
 settings.org_not_allowed_to_be_collaborator=As organizações não podem ser adicionadas como colaborador.
 settings.change_team_access_not_allowed=Alterar o acesso da equipa ao repositório foi restrito ao proprietário da organização
 settings.team_not_in_organization=A equipa não está na mesma organização que o repositório
@@ -2136,7 +2214,6 @@ settings.teams=Equipas
 settings.add_team=Adicionar equipa
 settings.add_team_duplicate=A equipa já tem o repositório
 settings.add_team_success=A equipa agora tem acesso ao repositório.
-settings.search_team=Procurar equipa…
 settings.change_team_permission_tip=A permissão da equipa é definida na página de configurações da equipa e não pode ter modificações específicas de cada repositório
 settings.delete_team_tip=Esta equipa tem acesso a todos os repositórios e não pode ser removida
 settings.remove_team_success=O acesso da equipa ao repositório foi removido.
@@ -2289,9 +2366,7 @@ settings.protect_whitelist_committers=Lista de permissões para restringir os en
 settings.protect_whitelist_committers_desc=Apenas os utilizadores ou equipas constantes na lista terão permissão para enviar para este ramo (mas não poderão fazer envios forçados).
 settings.protect_whitelist_deploy_keys=Dar permissão às chaves de instalação para terem acesso de escrita para enviar.
 settings.protect_whitelist_users=Utilizadores com permissão para enviar:
-settings.protect_whitelist_search_users=Procurar utilizadores…
 settings.protect_whitelist_teams=Equipas com permissão para enviar:
-settings.protect_whitelist_search_teams=Procurar equipas…
 settings.protect_merge_whitelist_committers=Habilitar lista de permissão para integrar
 settings.protect_merge_whitelist_committers_desc=Permitir que somente utilizadores ou equipas constantes na lista de permissão possam executar, neste ramo, integrações constantes em pedidos de integração.
 settings.protect_merge_whitelist_users=Utilizadores com permissão para executar integrações:
@@ -2312,6 +2387,8 @@ settings.protect_approvals_whitelist_users=Revisores com permissão:
 settings.protect_approvals_whitelist_teams=Equipas com permissão para rever:
 settings.dismiss_stale_approvals=Descartar aprovações obsoletas
 settings.dismiss_stale_approvals_desc=Quando novos cometimentos que mudam o conteúdo do pedido de integração forem enviados para o ramo, as aprovações antigas serão descartadas.
+settings.ignore_stale_approvals=Ignorar aprovações obsoletas
+settings.ignore_stale_approvals_desc=Não contar as aprovações feitas em cometimentos mais antigos (revisões obsoletas) para o número de aprovações do pedido de integração. É irrelevante se as revisões obsoletas já forem descartadas.
 settings.require_signed_commits=Exigir cometimentos assinados
 settings.require_signed_commits_desc=Rejeitar envios para este ramo que não estejam assinados ou que não sejam validáveis.
 settings.protect_branch_name_pattern=Padrão do nome do ramo protegido
@@ -2367,6 +2444,7 @@ settings.archive.error=Ocorreu um erro enquanto decorria o processo de arquivo d
 settings.archive.error_ismirror=Não pode arquivar um repositório que tenha sido replicado.
 settings.archive.branchsettings_unavailable=As configurações dos ramos não estão disponíveis quando o repositório está arquivado.
 settings.archive.tagsettings_unavailable=As configurações sobre etiquetas não estão disponíveis quando o repositório está arquivado.
+settings.archive.mirrors_unavailable=As réplicas não estão disponíveis se o repositório estiver arquivado.
 settings.unarchive.button=Desarquivar repositório
 settings.unarchive.header=Desarquivar este repositório
 settings.unarchive.text=Desarquivar o repositório irá restaurar a capacidade de receber cometimentos e envios, assim como novas questões e pedidos de integração.
@@ -2533,7 +2611,6 @@ branch.default_deletion_failed=O ramo "%s" é o ramo principal, não pode ser el
 branch.restore=`Restaurar o ramo "%s"`
 branch.download=`Descarregar o ramo "%s"`
 branch.rename=`Renomear ramo "%s"`
-branch.search=Pesquisar ramo
 branch.included_desc=Este ramo faz parte do ramo principal
 branch.included=Incluído
 branch.create_new_branch=Criar ramo a partir do ramo:
@@ -2564,6 +2641,16 @@ find_file.no_matching=Não foi encontrado qualquer ficheiro correspondente
 error.csv.too_large=Não é possível apresentar este ficheiro por ser demasiado grande.
 error.csv.unexpected=Não é possível apresentar este ficheiro porque contém um caractere inesperado na linha %d e coluna %d.
 error.csv.invalid_field_count=Não é possível apresentar este ficheiro porque tem um número errado de campos na linha %d.
+error.broken_git_hook=Os automatismos git deste repositório parecem estar danificados. Consulte a <a target="_blank" rel="noreferrer" href="%s">documentação</a> sobre como os consertar e depois envie alguns cometimentos para refrescar o estado.
+
+[graphs]
+component_loading=A carregar %s...
+component_loading_failed=Não foi possível carregar %s
+component_loading_info=Isto pode demorar um pouco…
+component_failed_to_load=Ocorreu um erro inesperado.
+code_frequency.what=frequência de programação
+contributors.what=contribuições
+recent_commits.what=cometimentos recentes
 
 [org]
 org_name_holder=Nome da organização
@@ -2669,7 +2756,6 @@ teams.write_permission_desc=Esta equipa atribui acesso de <strong>escrita</stron
 teams.admin_permission_desc=Esta equipa atribui o acesso de <strong>administração</strong>: os seus membros podem ler de, enviar para, e adicionar colaboradores aos repositórios da equipa.
 teams.create_repo_permission_desc=Adicionalmente, esta equipa atribui a permissão de <strong>criar repositórios</strong>: os seus membros podem criar novos repositórios na organização.
 teams.repositories=Repositórios da equipa
-teams.search_repo_placeholder=Procurar repositório…
 teams.remove_all_repos_title=Remover todos os repositórios da equipa
 teams.remove_all_repos_desc=Isto irá remover todos os repositórios da equipa.
 teams.add_all_repos_title=Adicionar todos os repositórios
@@ -2678,6 +2764,7 @@ teams.add_nonexistent_repo=O repositório que está a tentar adicionar não exis
 teams.add_duplicate_users=O utilizador já é um membro da equipa.
 teams.repos.none=Não há repositórios que possam ser acedidos por esta equipa.
 teams.members.none=Não há membros nesta equipa.
+teams.members.blocked_user=Não foi possível adicionar o/a utilizador/a porque essa operação foi bloqueada pela organização.
 teams.specific_repositories=Repositórios específicos
 teams.specific_repositories_helper=Os membros só terão acesso a repositórios explicitamente adicionados à equipa. Escolher isto <strong>não irá</strong> remover automaticamente os repositórios já adicionados com <i>Todos os repositórios</i>.
 teams.all_repositories=Todos os repositórios
@@ -2690,7 +2777,9 @@ teams.invite.by=Convidado(a) por %s
 teams.invite.description=Clique no botão abaixo para se juntar à equipa.
 
 [admin]
+maintenance=Manutenção
 dashboard=Painel de controlo
+self_check=Auto-verificação
 identity_access=Identidade e acesso
 users=Contas de utilizador
 organizations=Organizações
@@ -2701,6 +2790,8 @@ integrations=Integrações
 authentication=Fontes de autenticação
 emails=Emails do utilizador
 config=Configuração
+config_summary=Resumo
+config_settings=Configurações
 notices=Notificações do sistema
 monitor=Monitorização
 first_page=Primeira
@@ -2710,7 +2801,7 @@ settings=Configurações de administração
 
 dashboard.new_version_hint=O Gitea %s está disponível, você está a correr a versão %s. Verifique o <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blog</a> para mais detalhes.
 dashboard.statistic=Resumo
-dashboard.operations=Operações de manutenção
+dashboard.maintenance_operations=Operações de manutenção
 dashboard.system_status=Estado do sistema
 dashboard.operation_name=Nome da operação
 dashboard.operation_switch=Comutar
@@ -2736,6 +2827,7 @@ dashboard.delete_missing_repos=Eliminar todos os repositórios que não tenham o
 dashboard.delete_missing_repos.started=Foi iniciada a tarefa de eliminação de todos os repositórios que não têm ficheiros git.
 dashboard.delete_generated_repository_avatars=Eliminar avatares gerados do repositório
 dashboard.sync_repo_branches=Sincronizar ramos perdidos de dados do git para bases de dados
+dashboard.sync_repo_tags=Sincronizar etiquetas dos dados do git para a base de dados
 dashboard.update_mirrors=Sincronizar réplicas
 dashboard.repo_health_check=Verificar a saúde de todos os repositórios
 dashboard.check_repo_stats=Verificar as estatísticas de todos os repositórios
@@ -2790,6 +2882,7 @@ dashboard.stop_endless_tasks=Parar tarefas intermináveis
 dashboard.cancel_abandoned_jobs=Cancelar trabalhos abandonados
 dashboard.start_schedule_tasks=Iniciar tarefas de agendamento
 dashboard.sync_branch.started=Sincronização de ramos iniciada
+dashboard.sync_tag.started=Sincronização de etiquetas iniciada
 dashboard.rebuild_issue_indexer=Reconstruir indexador de questões
 
 users.user_manage_panel=Gestão das contas de utilizadores
@@ -2875,9 +2968,6 @@ repos.unadopted.no_more=Não foram encontrados mais repositórios não adoptados
 repos.owner=Proprietário(a)
 repos.name=Nome
 repos.private=Privado
-repos.watches=Vigilâncias
-repos.stars=Favoritos
-repos.forks=Derivações
 repos.issues=Questões
 repos.size=Tamanho
 repos.lfs_size=Tamanho do LFS
@@ -3002,7 +3092,7 @@ auths.tip.nextcloud=`Registe um novo consumidor OAuth na sua instância usando o
 auths.tip.dropbox=Crie uma nova aplicação em https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Registe uma nova aplicação em https://developers.facebook.com/apps e adicione o produto "Facebook Login"`
 auths.tip.github=Registe uma nova aplicação OAuth em https://github.com/settings/applications/new
-auths.tip.gitlab=Registe uma nova aplicação em https://gitlab.com/profile/applications
+auths.tip.gitlab_new=Registe uma nova aplicação em https://gitlab.com/-/profile/applications
 auths.tip.google_plus=Obtenha credenciais de cliente OAuth2 a partir da consola do Google API em https://console.developers.google.com/
 auths.tip.openid_connect=Use o URL da descoberta de conexão OpenID (<server>/.well-known/openid-configuration) para especificar os extremos
 auths.tip.twitter=`Vá a https://dev.twitter.com/apps, crie uma aplicação e certifique-se de que está habilitada a opção "Allow this application to be used to Sign in with Twitter"`
@@ -3138,6 +3228,7 @@ config.picture_config=Configuração da imagem e do avatar
 config.picture_service=Serviço de imagem
 config.disable_gravatar=Desabilitar o Gravatar
 config.enable_federated_avatar=Habilitar avatares federados
+config.open_with_editor_app_help=Os editores de "Abrir com" do menu de clonagem. Se for deixado em branco, será usado o predefinido. Expanda para ver o predefinido.
 
 config.git_config=Configuração Git
 config.git_disable_diff_highlight=Desabilitar o realce de sintaxe no diff
@@ -3216,6 +3307,14 @@ notices.desc=Descrição
 notices.op=Op.
 notices.delete_success=As notificações do sistema foram eliminadas.
 
+self_check.no_problem_found=Nenhum problema encontrado até agora.
+self_check.startup_warnings=Alertas do arranque:
+self_check.database_collation_mismatch=Supor que a base de dados usa a colação: %s
+self_check.database_collation_case_insensitive=A base de dados está a usar a colação %s, que é insensível à diferença entre maiúsculas e minúsculas. Embora o Gitea possa trabalhar com ela, pode haver alguns casos raros que não funcionem como esperado.
+self_check.database_inconsistent_collation_columns=A base de dados está a usar a colação %s, mas estas colunas estão a usar colações diferentes. Isso poderá causar alguns problemas inesperados.
+self_check.database_fix_mysql=Para utilizadores do MySQL/MariaDB, pode usar o comando "gitea doctor convert" para resolver os problemas de colação. Também pode resolver o problema com comandos SQL "ALTER ... COLLATE ..." aplicados manualmente.
+self_check.database_fix_mssql=Para utilizadores do MSSQL só pode resolver o problema aplicando comandos SQL "ALTER ... COLLATE ..." manualmente, por enquanto.
+
 [action]
 create_repo=criou o repositório <a href="%s">%s</a>
 rename_repo=renomeou o repositório de <code>%[1]s</code> para <a href="%[2]s">%[3]s</a>
@@ -3400,6 +3499,9 @@ rpm.registry=Configurar este registo usando a linha de comandos:
 rpm.distros.redhat=em distribuições baseadas no RedHat
 rpm.distros.suse=em distribuições baseadas no SUSE
 rpm.install=Para instalar o pacote, execute o seguinte comando:
+rpm.repository=Informação do repositório
+rpm.repository.architectures=Arquitecturas
+rpm.repository.multiple_groups=Este pacote está disponível em vários grupos.
 rubygems.install=Para instalar o pacote usando o gem, execute o seguinte comando:
 rubygems.install2=ou adicione-o ao ficheiro <code>Gemfile</code>:
 rubygems.dependencies.runtime=Dependências do tempo de execução (runtime)
@@ -3526,14 +3628,15 @@ runs.scheduled=Agendadas
 runs.pushed_by=enviado por
 runs.invalid_workflow_helper=O ficheiro de configuração da sequência de trabalho é inválido. Verifique o seu ficheiro de configuração: %s
 runs.no_matching_online_runner_helper=Não existem executores ligados que tenham o rótulo %s
+runs.no_job_without_needs=A sequência de trabalho tem que conter pelo menos um trabalho sem dependências.
 runs.actor=Interveniente
 runs.status=Estado
 runs.actors_no_select=Todos os intervenientes
 runs.status_no_select=Todos os estados
 runs.no_results=Nenhum resultado obtido.
 runs.no_workflows=Ainda não há sequências de trabalho.
-runs.no_workflows.quick_start=Não sabe como começar com o Gitea Action? Veja o <a target="_blank" rel="noopener noreferrer" href="%s">guia de iniciação rápida</a>.
-runs.no_workflows.documentation=Para mais informação sobre o Gitea Action, veja <a target="_blank" rel="noopener noreferrer" href="%s">a documentação</a>.
+runs.no_workflows.quick_start=Não sabe como começar com o Gitea Actions? Veja o <a target="_blank" rel="noopener noreferrer" href="%s">guia de inicio rápido</a>.
+runs.no_workflows.documentation=Para mais informação sobre o Gitea Actions veja <a target="_blank" rel="noopener noreferrer" href="%s">a documentação</a>.
 runs.no_runs=A sequência de trabalho ainda não foi executada.
 runs.empty_commit_message=(mensagem de cometimento vazia)
 
@@ -3552,7 +3655,7 @@ variables.none=Ainda não há variáveis.
 variables.deletion=Remover variável
 variables.deletion.description=Remover uma variável é permanente e não pode ser revertido. Quer continuar?
 variables.description=As variáveis serão transmitidas a certas operações e não poderão ser lidas de outra forma.
-variables.id_not_exist=A variável com o id %d não existe.
+variables.id_not_exist=A variável com o ID %d não existe.
 variables.edit=Editar variável
 variables.deletion.failed=Falha ao remover a variável.
 variables.deletion.success=A variável foi removida.
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index 0a466854d0..818dad1147 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -139,6 +139,15 @@ confirm_delete_selected=Вы уверены, что хотите удалить
 name=Название
 value=Значение
 
+filter=Фильтр
+filter.is_archived=Архивировано
+filter.is_template=Шаблон
+filter.public=Публичный
+filter.private=Личный
+
+
+[search]
+
 [aria]
 navbar=Панель навигации
 footer=Подвал
@@ -312,7 +321,6 @@ collaborative_repos=Совместные репозитории
 my_orgs=Мои организации
 my_mirrors=Мои зеркала
 view_home=Показать %s
-search_repos=Поиск репозитория…
 filter=Другие фильтры
 filter_by_team_repositories=Фильтровать по репозиториям команды
 feed_of=Лента «%s»
@@ -333,20 +341,8 @@ issues.in_your_repos=В ваших репозиториях
 repos=Репозитории
 users=Пользователи
 organizations=Организации
-search=Поиск
 go_to=Перейти к
 code=Код
-search.type.tooltip=Тип поиска
-search.fuzzy=Неточный
-search.fuzzy.tooltip=Включать результаты, которые не полностью соответствуют поисковому запросу
-search.match=Соответствие
-search.match.tooltip=Включать только результаты, которые точно соответствуют поисковому запросу
-code_search_unavailable=В настоящее время поиск по коду недоступен. Обратитесь к администратору сайта.
-repo_no_results=Подходящие репозитории не найдены.
-user_no_results=Подходящие пользователи не найдены.
-org_no_results=Подходящие организации не найдены.
-code_no_results=Соответствующий поисковому запросу исходный код не найден.
-code_search_results=Результаты поиска «%s»
 code_last_indexed_at=Последний проиндексированный %s
 relevant_repositories_tooltip=Репозитории, являющиеся ответвлениями или не имеющие ни темы, ни значка, ни описания, скрыты.
 relevant_repositories=Показаны только релевантные репозитории, <a href="%s">показать результаты без фильтрации</a>.
@@ -364,7 +360,6 @@ forgot_password_title=Восстановить пароль
 forgot_password=Забыли пароль?
 sign_up_now=Нужен аккаунт? Зарегистрируйтесь.
 sign_up_successful=Учётная запись успешно создана. Добро пожаловать!
-confirmation_mail_sent_prompt=Новое письмо для подтверждения направлено на <b>%s</b>. Пожалуйста, проверьте ваш почтовый ящик в течение %s для завершения регистрации.
 must_change_password=Обновить пароль
 allow_password_change=Требовать смену пароля пользователем (рекомендуется)
 reset_password_mail_sent_prompt=Письмо с подтверждением отправлено на <b>%s</b>. Пожалуйста, проверьте входящую почту в течение %s, чтобы завершить процесс восстановления аккаунта.
@@ -586,6 +581,7 @@ org_still_own_packages=Эта организация всё ещё владее
 
 target_branch_not_exist=Целевая ветка не существует.
 
+
 [user]
 change_avatar=Изменить свой аватар…
 joined_on=Присоединил(ся/ась) %s
@@ -611,6 +607,7 @@ form.name_reserved=Имя пользователя «%s» зарезервиро
 form.name_pattern_not_allowed=Шаблон «%s» не допускается в имени пользователя.
 form.name_chars_not_allowed=Имя пользователя «%s» содержит недопустимые символы.
 
+
 [settings]
 profile=Профиль
 account=Аккаунт
@@ -754,7 +751,6 @@ gpg_invalid_token_signature=Предоставленный ключ GPG, под
 gpg_token_required=Вы должны предоставить подпись для токена ниже
 gpg_token=Токен
 gpg_token_help=Вы можете сгенерировать подпись с помощью:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Текстовая подпись GPG
 key_signature_gpg_placeholder=Начинается с '-----BEGIN PGP SIGNATURE-----'
 verify_gpg_key_success=Ключ GPG «%s» верифицирован.
@@ -941,7 +937,6 @@ fork_visibility_helper=Видимость форкнутого репозито
 fork_branch=Ветка для клонирования в форк
 all_branches=Все ветки
 use_template=Использовать этот шаблон
-clone_in_vsc=Клонировать в VS Code
 download_zip=Скачать ZIP
 download_tar=Скачать TAR.GZ
 download_bundle=Скачать BUNDLE
@@ -1248,9 +1243,7 @@ commits.desc=Просмотр истории изменений исходног
 commits.commits=Коммитов
 commits.no_commits=Нет общих коммитов. «%s» и «%s» имеют совершенно разные истории.
 commits.nothing_to_compare=Эти ветки одинаковы.
-commits.search=Поиск коммитов…
 commits.search.tooltip=Можно предварять ключевые слова префиксами "author:", "committer:", "after:", или "before:", например "revert author:Alice before:2019-01-13".
-commits.find=Поиск
 commits.search_all=Все ветки
 commits.author=Автор
 commits.message=Сообщение
@@ -1300,7 +1293,6 @@ projects.type.basic_kanban=Обычный Канбан
 projects.type.bug_triage=Планирование работы с багами
 projects.template.desc=Шаблон проекта
 projects.template.desc_helper=Выберите шаблон проекта для начала
-projects.type.uncategorized=Без категории
 projects.column.edit=Изменить столбец
 projects.column.edit_title=Название
 projects.column.new_title=Название
@@ -1308,10 +1300,7 @@ projects.column.new_submit=Создать столбец
 projects.column.new=Новый столбец
 projects.column.set_default=Установить по умолчанию
 projects.column.set_default_desc=Назначить этот столбец по умолчанию для неклассифицированных задач и запросов на слияние
-projects.column.unset_default=Снять установку по умолчанию
-projects.column.unset_default_desc=Снять установку этого столбца по умолчанию
 projects.column.delete=Удалить столбец
-projects.column.deletion_desc=При удалении столбца проекта все связанные задачи перемещаются в 'Без категории'. Продолжить?
 projects.column.color=Цвет
 projects.open=Открыть
 projects.close=Закрыть
@@ -1423,7 +1412,6 @@ issues.filter_sort.moststars=Больше звезд
 issues.filter_sort.feweststars=Меньше звезд
 issues.filter_sort.mostforks=Больше форков
 issues.filter_sort.fewestforks=Меньше форков
-issues.keyword_search_unavailable=В настоящее время поиск по ключевым словам недоступен. Обратитесь к администратору сайта.
 issues.action_open=Открыть
 issues.action_close=Закрыть
 issues.action_label=Метка
@@ -1672,7 +1660,6 @@ pulls.compare_compare=взять из
 pulls.switch_comparison_type=Переключить тип сравнения
 pulls.switch_head_and_base=Поменять исходную и целевую ветки местами
 pulls.filter_branch=Фильтр по ветке
-pulls.no_results=Результатов не найдено.
 pulls.show_all_commits=Показать все коммиты
 pulls.show_changes_since_your_last_review=Показать изменения с момента вашего последнего отзыва
 pulls.showing_only_single_commit=Показать только изменения коммита %[1]s
@@ -1926,16 +1913,7 @@ activity.git_stats_and_deletions=и
 activity.git_stats_deletion_1=%d удаление
 activity.git_stats_deletion_n=%d удалений
 
-search=Поиск
-search.search_repo=Поиск по репозиторию
-search.type.tooltip=Тип поиска
-search.fuzzy=Неточный
-search.fuzzy.tooltip=Включать результаты, которые не полностью соответствуют поисковому запросу
-search.match=Соответствие
-search.match.tooltip=Включать только результаты, которые точно соответствуют поисковому запросу
-search.results=Результаты поиска "%s" в <a href="%s">%s</a>
-search.code_no_results=Не найдено исходного кода, соответствующего поисковому запросу.
-search.code_search_unavailable=В настоящее время поиск по коду недоступен. Обратитесь к администратору сайта.
+contributors.contribution_type.commits=коммитов
 
 settings=Настройки
 settings.desc=В настройках вы можете менять различные параметры этого репозитория
@@ -2012,6 +1990,7 @@ settings.pulls.default_allow_edits_from_maintainers=По умолчанию ра
 settings.releases_desc=Включить релизы
 settings.packages_desc=Включить реестр пакетов
 settings.projects_desc=Включить проекты репозитория
+settings.projects_mode_all=Все проекты
 settings.actions_desc=Включить действия репозитория
 settings.admin_settings=Настройки администратора
 settings.admin_enable_health_check=Выполнять проверки целостности этого репозитория (git fsck)
@@ -2086,7 +2065,6 @@ settings.delete_collaborator=Удалить
 settings.collaborator_deletion=Удалить соавтора
 settings.collaborator_deletion_desc=Этот пользователь больше не будет иметь доступа для совместной работы в этом репозитории после удаления. Вы хотите продолжить?
 settings.remove_collaborator_success=Соавтор удалён.
-settings.search_user_placeholder=Поиск пользователя…
 settings.org_not_allowed_to_be_collaborator=Организации не могут быть добавлены как соавторы.
 settings.change_team_access_not_allowed=Доступ к репозиторию команде ограничен владельцем организации
 settings.team_not_in_organization=Команда не в той же организации, что и репозиторий
@@ -2094,7 +2072,6 @@ settings.teams=Команды
 settings.add_team=Добавить команду
 settings.add_team_duplicate=Команда уже имеет репозиторий
 settings.add_team_success=Команда теперь имеет доступ к репозиторию.
-settings.search_team=Поиск команды…
 settings.change_team_permission_tip=Разрешение команды установлено на странице настройки команды и не может быть изменено для каждого репозитория
 settings.delete_team_tip=Эта команда имеет доступ ко всем репозиториям и не может быть удалена
 settings.remove_team_success=Доступ команды к репозиторию удалён.
@@ -2245,9 +2222,7 @@ settings.protect_whitelist_committers=Ограничение отправки п
 settings.protect_whitelist_committers_desc=Только пользователям или командам из белого списка будет разрешена отправка изменений в эту ветку (но не принудительная отправка).
 settings.protect_whitelist_deploy_keys=Белый список развёртываемых ключей с доступом на запись в push.
 settings.protect_whitelist_users=Пользователи, которые могут отправлять изменения в эту ветку:
-settings.protect_whitelist_search_users=Поиск пользователей…
 settings.protect_whitelist_teams=Команды, члены которых могут отправлять изменения в эту ветку:
-settings.protect_whitelist_search_teams=Поиск команд…
 settings.protect_merge_whitelist_committers=Ограничить право на слияние белым списком
 settings.protect_merge_whitelist_committers_desc=Разрешить принимать запросы на слияние в эту ветку только пользователям и командам из «белого списка».
 settings.protect_merge_whitelist_users=Пользователи с правом на слияние:
@@ -2483,7 +2458,6 @@ branch.default_deletion_failed=Ветка «%s» является веткой 
 branch.restore=Восстановить ветку «%s»
 branch.download=Скачать ветку «%s»
 branch.rename=Переименовать ветку «%s»
-branch.search=Поиск ветки
 branch.included_desc=Эта ветка является частью ветки по умолчанию
 branch.included=Включено
 branch.create_new_branch=Создать ветку из ветви:
@@ -2515,6 +2489,8 @@ error.csv.too_large=Не удается отобразить этот файл,
 error.csv.unexpected=Не удается отобразить этот файл, потому что он содержит неожиданный символ в строке %d и столбце %d.
 error.csv.invalid_field_count=Не удается отобразить этот файл, потому что он имеет неправильное количество полей в строке %d.
 
+[graphs]
+
 [org]
 org_name_holder=Название организации
 org_full_name_holder=Полное название организации
@@ -2618,7 +2594,6 @@ teams.write_permission_desc=Эта команда предоставляет д
 teams.admin_permission_desc=Эта команда даёт <strong>административный</strong> доступ: участники могут читать, отправлять изменения и добавлять соавторов к её репозиториям.
 teams.create_repo_permission_desc=Кроме того, эта команда предоставляет право <strong>Создание репозитория</strong>: члены команды могут создавать новые репозитории в организации.
 teams.repositories=Репозитории группы разработки
-teams.search_repo_placeholder=Поиск репозитория…
 teams.remove_all_repos_title=Удалить все репозитории команды
 teams.remove_all_repos_desc=Удаляет все репозитории из команды.
 teams.add_all_repos_title=Добавить все репозитории
@@ -2649,6 +2624,8 @@ integrations=Интеграции
 authentication=Аутентификация
 emails=Адреса эл. почты пользователей
 config=Конфигурация
+config_summary=Статистика
+config_settings=Настройки
 notices=Системные уведомления
 monitor=Мониторинг
 first_page=Первая
@@ -2657,7 +2634,6 @@ total=Всего: %d
 
 dashboard.new_version_hint=Доступна новая версия Gitea %s, вы используете %s. Более подробную информацию читайте в <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">блоге</a>.
 dashboard.statistic=Статистика
-dashboard.operations=Операции
 dashboard.system_status=Состояние системы
 dashboard.operation_name=Имя операции
 dashboard.operation_switch=Переключить
@@ -2817,9 +2793,6 @@ repos.unadopted.no_more=Больше непринятых репозиторие
 repos.owner=Владелец
 repos.name=Название
 repos.private=Личный
-repos.watches=Следят
-repos.stars=Звезды
-repos.forks=Форки
 repos.issues=Задачи
 repos.size=Размер
 repos.lfs_size=Размер LFS
@@ -2941,7 +2914,6 @@ auths.tip.nextcloud=`Зарегистрируйте нового потреби
 auths.tip.dropbox=Добавьте новое приложение на https://www.dropbox.com/developers/apps
 auths.tip.facebook=Зарегистрируйте новое приложение на https://developers.facebook.com/apps и добавьте модуль «Facebook Login»
 auths.tip.github=Добавьте OAuth приложение на https://github.com/settings/applications/new
-auths.tip.gitlab=Добавьте новое приложение на https://gitlab.com/profile/applications
 auths.tip.google_plus=Получите учётные данные клиента OAuth2 в консоли Google API на странице https://console.developers.google.com/
 auths.tip.openid_connect=Используйте OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) для автоматической настройки входа OAuth
 auths.tip.twitter=Перейдите на https://dev.twitter.com/apps, создайте приложение и убедитесь, что включена опция «Разрешить это приложение для входа в систему с помощью Twitter»
@@ -3153,6 +3125,7 @@ notices.desc=Описание
 notices.op=Oп.
 notices.delete_success=Уведомления системы были удалены.
 
+
 [action]
 create_repo=создал(а) репозиторий <a href="%s"> %s</a>
 rename_repo=переименовал(а) репозиторий из <code>%[1]s</code> на <a href="%[2]s">%[3]s</a>
@@ -3337,6 +3310,8 @@ rpm.registry=Настроить реестр из командной строк
 rpm.distros.redhat=на дистрибутивах семейства RedHat
 rpm.distros.suse=на дистрибутивах семейства SUSE
 rpm.install=Чтобы установить пакет, выполните следующую команду:
+rpm.repository=О репозитории
+rpm.repository.architectures=Архитектуры
 rubygems.install=Чтобы установить пакет с помощью gem, выполните следующую команду:
 rubygems.install2=или добавьте его в Gemfile:
 rubygems.dependencies.runtime=Зависимости времени выполнения
@@ -3464,8 +3439,6 @@ runs.status=Статус
 runs.actors_no_select=Все акторы
 runs.no_results=Ничего не найдено.
 runs.no_workflows=Пока нет рабочих процессов.
-runs.no_workflows.quick_start=Не знаете, как начать использовать Действия Gitea? Читайте <a target="_blank" rel="noopener noreferrer" href="%s">руководство по быстрому старту</a>.
-runs.no_workflows.documentation=Чтобы узнать больше о Действиях Gitea, читайте <a target="_blank" rel="noopener noreferrer" href="%s">документацию</a>.
 runs.no_runs=Рабочий поток ещё не запускался.
 runs.empty_commit_message=(пустое сообщение коммита)
 
@@ -3484,7 +3457,6 @@ variables.none=Переменных пока нет.
 variables.deletion=Удалить переменную
 variables.deletion.description=Удаление переменной необратимо, его нельзя отменить. Продолжить?
 variables.description=Переменные будут передаваться определенным действиям и не могут быть прочитаны иначе.
-variables.id_not_exist=Переменная с идентификатором %d не существует.
 variables.edit=Изменить переменную
 variables.deletion.failed=Не удалось удалить переменную.
 variables.deletion.success=Переменная удалена.
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index 6d70bc385a..99559802c5 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -100,6 +100,15 @@ concept_user_organization=සංවිධානය
 
 name=නම
 
+filter=පෙරහන
+filter.is_archived=සංරක්ෂිත
+filter.is_template=සැකිලි
+filter.public=ප්‍රසිද්ධ
+filter.private=පෞද්ගලික
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -223,7 +232,6 @@ collaborative_repos=සහයෝගී ගබඩාවලදී
 my_orgs=මාගේ සංවිධාන
 my_mirrors=මගේ දර්පණ
 view_home=%s දකින්න
-search_repos=ගබඩාවක් සොයා ගන්න…
 filter=වෙනත් පෙරහන්
 filter_by_team_repositories=කණ්ඩායම් කෝෂ්ඨ අනුව පෙරන්න
 
@@ -243,13 +251,7 @@ issues.in_your_repos=ඔබගේ කෝෂ්ඨවල
 repos=කෝෂ්ඨ
 users=පරිශීලකයින්
 organizations=සංවිධාන
-search=සොයන්න
 code=කේතය
-search.match=තරගය
-repo_no_results=ගැලපෙන ගබඩාවක් හමු නොවීය.
-user_no_results=ගැලපෙන පරිශීලකයින් හමු නොවීය.
-org_no_results=ගැලපෙන සංවිධාන හමු නොවීය.
-code_no_results=ඔබගේ සෙවුම් පදය ගැලපෙන ප්රභව කේතයක් නොමැත.
 code_last_indexed_at=අවසන් සුචිගත %s
 
 [auth]
@@ -262,7 +264,6 @@ remember_me=උපාංගය මතක තබාගන්න
 forgot_password_title=මුරපදය අමතක වුණා
 forgot_password=මුරපදය අමතක වුණා ද?
 sign_up_now=ගිණුමක් ඇවැසිද? දැන් ලියාපදිංචි වන්න.
-confirmation_mail_sent_prompt=නව තහවුරු කිරීමේ විද්යුත් තැපෑලක් <b>%s</b>වෙත යවා ඇත. ලියාපදිංචි කිරීමේ ක්රියාවලිය සම්පූර්ණ කිරීම සඳහා කරුණාකර ඊළඟ %s තුළ ඔබගේ එන ලිපි පරීක්ෂා කරන්න.
 must_change_password=මුරපදය යාවත්කාල කරන්න
 allow_password_change=මුරපදය වෙනස් කිරීමට පරිශීලකයාට අවශ්ය වේ (නිර්දේශිත)
 reset_password_mail_sent_prompt=තහවුරු කිරීමේ විද්යුත් තැපෑලක් <b>%s</b>වෙත යවා ඇත. ඊළඟ තුළ ඔබගේ එන ලිපි පරීක්ෂා කරන්න %s ගිණුම යථා ක්රියාවලිය සම්පූර්ණ කිරීම සඳහා.
@@ -450,6 +451,7 @@ auth_failed=සත්යාපන අසමත් විය: %v
 
 target_branch_not_exist=ඉලක්කගත ශාඛාව නොපවතී.
 
+
 [user]
 change_avatar=ඔබගේ අවතාරය වෙනස් කරන්න…
 repositories=කෝෂ්ඨ
@@ -466,6 +468,7 @@ user_bio=චරිතාපදානය
 disabled_public_activity=මෙම පරිශීලකයා ක්රියාකාරකම්වල මහජන දෘශ්යතාව අක්රීය කර ඇත.
 
 
+
 [settings]
 profile=පැතිකඩ
 account=ගිණුම
@@ -577,7 +580,6 @@ gpg_invalid_token_signature=සපයන ලද GPG යතුර, අත්ස
 gpg_token_required=පහත ටෝකනය සඳහා ඔබ අත්සනක් ලබා දිය යුතුය
 gpg_token=ටෝකනය
 gpg_token_help=ඔබට අත්සනක් ජනනය කළ හැකිය:
-gpg_token_code=දෝංකාරය "%s" | gpg -a -පැහැර හැරීම-යතුර %s —වෙන්ච-සිග්
 gpg_token_signature=සන්නද්ධ GPG අත්සන
 key_signature_gpg_placeholder=ආරම්භ වන්නේ '—ආරම්භ කරන්න PGP සිග්නේටුර්—'
 ssh_key_verified=සත්යාපිත යතුර
@@ -716,7 +718,6 @@ fork_repo=දෙබලක ගබඩාව
 fork_from=සිට දෙබලක
 fork_visibility_helper=ව්යාජ ගබඩාවේ දෘශ්යතාව වෙනස් කළ නොහැක.
 use_template=මෙම අච්චුව භාවිතා කරන්න
-clone_in_vsc=VS කේතය පරිගණක ක්රිඩාවට සමාන
 download_zip=ZIP බාගන්න
 download_tar=TAR.GZ බාගන්න
 download_bundle=බණ්ඩලය බාගත කරන්න
@@ -942,8 +943,6 @@ editor.require_signed_commit=ශාඛාවට අත්සන් කළ කැ
 commits.desc=මූලාශ්ර කේත වෙනස් කිරීමේ ඉතිහාසය පිරික්සන්න.
 commits.commits=විවරයන්
 commits.nothing_to_compare=මෙම ශාඛා සමාන වේ.
-commits.search=සෙවුම් වාර…
-commits.find=සොයන්න
 commits.search_all=සියළුම ශාඛා
 commits.author=කතෘ
 commits.message=පණිවිඩය
@@ -980,7 +979,6 @@ projects.type.basic_kanban=මූලික කන්ෙවනි
 projects.type.bug_triage=දෝෂ ට්රයිජ්
 projects.template.desc=ව්යාපෘති සැකිල්ල
 projects.template.desc_helper=ආරම්භ කිරීම සඳහා ව්යාපෘති සැකිල්ලක් තෝරන්න
-projects.type.uncategorized=ප්‍රවර්ග ගත නැති
 projects.column.edit_title=නම
 projects.column.new_title=නම
 projects.column.color=වර්ණය
@@ -1262,7 +1260,6 @@ pulls.compare_compare=සිට අදින්න
 pulls.switch_comparison_type=ස්විච් සංසන්දනය වර්ගය
 pulls.switch_head_and_base=හිස සහ පාදය මාරු කරන්න
 pulls.filter_branch=ශාඛාව පෙරන්න
-pulls.no_results=ප්රතිඵල සොයාගත නොහැකි විය.
 pulls.nothing_to_compare=මෙම ශාඛා සමාන වේ. අදින්න ඉල්ලීමක් නිර්මාණය කිරීමට අවශ්ය නැත.
 pulls.nothing_to_compare_and_allow_empty_pr=මෙම ශාඛා සමාන වේ. මෙම මහජන සම්බන්ධතා හිස් වනු ඇත.
 pulls.has_pull_request=`මෙම ශාඛා අතර අදින්න ඉල්ලීම දැනටමත් පවතී: <a href="%[1]s">%[2]s #%[3]d</a>`
@@ -1459,12 +1456,7 @@ activity.git_stats_and_deletions=සහ
 activity.git_stats_deletion_1=%d මකාදැමීම
 activity.git_stats_deletion_n=%d මකාදැමීම්
 
-search=සොයන්න
-search.search_repo=කෝෂ්ඨය සොයන්න
-search.fuzzy=සිනිඳු
-search.match=තරගය
-search.results=<a href="%s">%s</a> හි "%s" සඳහා සෙවුම් ප්‍රතිඵල
-search.code_no_results=ඔබගේ සෙවුම් පදය ගැලපෙන ප්රභව කේතයක් නොමැත.
+contributors.contribution_type.commits=විවරයන්
 
 settings=සැකසුම්
 settings.desc=සැකසුම් යනු ගබඩාව සඳහා සැකසුම් කළමනාකරණය කළ හැකි ස්ථානයයි
@@ -1581,7 +1573,6 @@ settings.delete_collaborator=ඉවත් කරන්න
 settings.collaborator_deletion=සහයෝගිතාකරු ඉවත් කරන්න
 settings.collaborator_deletion_desc=සහයෝගිතාකරුවෙකු ඉවත් කිරීම මෙම ගබඩාවට ඔවුන්ගේ ප්රවේශය අවලංගු කරනු ඇත. දිගටම?
 settings.remove_collaborator_success=සහයෝගිතාකරු ඉවත් කර ඇත.
-settings.search_user_placeholder=පරිශීලක සොයන්න…
 settings.org_not_allowed_to_be_collaborator=සහයෝගීකයෙකු ලෙස සංවිධාන එකතු කළ නොහැක.
 settings.change_team_access_not_allowed=ගබඩාව සඳහා කණ්ඩායම් ප්රවේශය වෙනස් කිරීම සංවිධාන හිමිකරුට සීමා කර ඇත
 settings.team_not_in_organization=මෙම කණ්ඩායම ගබඩාවේ එකම සංවිධානයේ නොමැත
@@ -1589,7 +1580,6 @@ settings.teams=කණ්ඩායම්
 settings.add_team=කණ්ඩායම එකතු කරන්න
 settings.add_team_duplicate=කණ්ඩායම දැනටමත් ගබඩාවක් ඇත
 settings.add_team_success=කණ්ඩායමට දැන් කෝෂ්ඨයට ප්‍රවේශය ඇත.
-settings.search_team=කණ්ඩායම සොයන්න…
 settings.change_team_permission_tip=කණ්ඩායමේ අවසරය කණ්ඩායම් සැකසුම් පිටුවේ සකසන අතර කෝෂ්ඨය අනුව වෙනස් කළ නොහැකිය
 settings.delete_team_tip=මෙම කණ්ඩායම සියළුම කෝෂ්ඨවලට ප්‍රවේශය ඇති අතර ඉවත් කළ නොහැකිය
 settings.remove_team_success=කෝෂ්ඨය වෙත කණ්ඩායමේ ප්‍රවේශය ඉවත් කර ඇත.
@@ -1706,9 +1696,7 @@ settings.protect_whitelist_committers=වයිට්ලිස්ට් සී
 settings.protect_whitelist_committers_desc=මෙම ශාඛාව වෙත තල්ලු කිරීමට අවසර ඇත්තේ වයිට්ලිස්ට් පරිශීලකයින්ට හෝ කණ්ඩායම්වලට පමණි (නමුත් බල තල්ලුව නොවේ).
 settings.protect_whitelist_deploy_keys=වයිට්ලිස්ට් තල්ලු කිරීමට ලිවීමේ ප්රවේශය සහිත යතුරු යොදවන්න.
 settings.protect_whitelist_users=තල්ලු කිරීම සඳහා වයිට්ලිස්ට් පරිශීලකයින්:
-settings.protect_whitelist_search_users=පරිශීලකයින් සොයන්න…
 settings.protect_whitelist_teams=තල්ලු කිරීම සඳහා වයිට්ලිස්ට් කණ්ඩායම්:
-settings.protect_whitelist_search_teams=කණ්ඩායම් සොයන්න…
 settings.protect_merge_whitelist_committers=ඒකාබද්ධ වයිට්ලිස්ට් සක්රීය කරන්න
 settings.protect_merge_whitelist_committers_desc=මෙම ශාඛාවට ඇද ගැනීමේ ඉල්ලීම් ඒකාබද්ධ කිරීමට සුදු පැහැති පරිශීලකයින්ට හෝ කණ්ඩායම්වලට පමණක් ඉඩ දෙන්න.
 settings.protect_merge_whitelist_users=ඒකාබද්ධ කිරීම සඳහා Whitelisted පරිශීලකයන්:
@@ -1910,6 +1898,8 @@ error.csv.too_large=එය ඉතා විශාල නිසා මෙම ග
 error.csv.unexpected=%d පේළියේ සහ %dතීරුවේ අනපේක්ෂිත චරිතයක් අඩංගු බැවින් මෙම ගොනුව විදැහුම්කරණය කළ නොහැක.
 error.csv.invalid_field_count=මෙම ගොනුව රේඛාවේ වැරදි ක්ෂේත්ර සංඛ්යාවක් ඇති බැවින් එය විදැහුම්කරණය කළ නොහැක %d.
 
+[graphs]
+
 [org]
 org_name_holder=සංවිධානයේ නම
 org_full_name_holder=සංවිධානයේ සම්පූර්ණ නම
@@ -2001,7 +1991,6 @@ teams.write_permission_desc=මෙම කණ්ඩායම ප්රදාන
 teams.admin_permission_desc=මෙම කණ්ඩායම <strong>පරිපාලක</strong> ප්රවේශය ලබා දෙයි: සාමාජිකයින්ට කියවීමට, කණ්ඩායම් ගබඩාවන්ට සහයෝගීකයින් වෙත තල්ලු කිරීමට සහ එකතු කිරීමට හැකිය.
 teams.create_repo_permission_desc=මීට අමතරව, මෙම කණ්ඩායම <strong>ලබා දෙයි ගබඩාව සාදන්න</strong> අවසරය: සාමාජිකයින්ට සංවිධානයේ නව ගබඩාවක් නිර්මාණය කළ හැකිය.
 teams.repositories=කණ්ඩායම් කෝෂ්ඨ
-teams.search_repo_placeholder=කෝෂ්ඨය සොයන්න…
 teams.remove_all_repos_title=සියළුම කණ්ඩායම් කෝෂ්ඨ ඉවත් කරන්න
 teams.remove_all_repos_desc=මෙය කණ්ඩායමෙන් සියළුම කෝෂ්ඨ ඉවත් කෙරෙනු ඇත.
 teams.add_all_repos_title=සියළුම කෝෂ්ඨ එක්කරන්න
@@ -2026,6 +2015,8 @@ hooks=වෙබ්කොකු
 authentication=සත්යාපන ප්රභවයන්
 emails=පරිශීලක වි-තැපැල්
 config=වින්‍යාසය
+config_summary=සාරාංශය
+config_settings=සැකසුම්
 notices=පද්ධතියේ දැන්වීම්
 monitor=අධීක්ෂණය
 first_page=පළමු
@@ -2033,7 +2024,6 @@ last_page=පසුගිය
 total=මුළු: %d
 
 dashboard.statistic=සාරාංශය
-dashboard.operations=නඩත්තු මෙහෙයුම්
 dashboard.system_status=පද්ධතියේ තත්වය
 dashboard.operation_name=මෙහෙයුමේ නම
 dashboard.operation_switch=මාරුවන්න
@@ -2173,9 +2163,6 @@ repos.unadopted.no_more=තවත් සම්මත නොකළ ගබඩා
 repos.owner=හිමිකරු
 repos.name=නම
 repos.private=පෞද්ගලික
-repos.watches=අත් ඔරලෝසු
-repos.stars=තරු
-repos.forks=දෙබලක
 repos.issues=ගැටළු
 repos.size=ප්‍රමාණය
 
@@ -2273,7 +2260,6 @@ auths.tip.nextcloud=පහත සඳහන් මෙනුව භාවිතා
 auths.tip.dropbox=https://www.dropbox.com/developers/apps හි නව යෙදුමක් සාදන්න
 auths.tip.facebook=https://developers.facebook.com/apps හි නව යෙදුමක් ලියාපදිංචි කර නිෂ්පාදනය එකතු කරන්න “ෆේස්බුක් ලොගින් වන්න”
 auths.tip.github=https://github.com/settings/applications/new හි නව OAUTH අයදුම්පතක් ලියාපදිංචි කරන්න
-auths.tip.gitlab=https://gitlab.com/profile/applications හි නව අයදුම්පතක් ලියාපදිංචි කරන්න
 auths.tip.google_plus=ගූගල් API කොන්සෝලය වෙතින් OUT2 සේවාදායක අක්තපත්ර ලබා ගන්න https://console.developers.google.com/
 auths.tip.openid_connect=අන්ත ලක්ෂ්ය නියම කිරීම සඳහා OpenID Connect ඩිස්කවරි URL (<server>/.හොඳින් දැන /openid-වින්යාසය) භාවිතා කරන්න
 auths.tip.twitter=https://dev.twitter.com/apps වෙත යන්න, යෙදුමක් සාදන්න සහ “මෙම යෙදුම ට්විටර් සමඟ පුරනය වීමට භාවිතා කිරීමට ඉඩ දෙන්න” විකල්පය සක්රීය කර ඇති බවට සහතික වන්න
@@ -2456,6 +2442,7 @@ notices.desc=සවිස්තරය
 notices.op=ඔප්.
 notices.delete_success=පද්ධති දැන්වීම් මකා දමා ඇත.
 
+
 [action]
 create_repo=නිර්මිත ගබඩාව <a href="%s">%s</a>
 rename_repo=<code>%[1]s</code> සිට <a href="%[2]s">%[3]s</a>දක්වා නම් කරන ලද ගබඩාව
diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini
index 1c3ca5ae43..b468b55283 100644
--- a/options/locale/locale_sk-SK.ini
+++ b/options/locale/locale_sk-SK.ini
@@ -140,6 +140,13 @@ confirm_delete_selected=Potvrdzujete zmazanie všetkých vybraných položiek?
 name=Meno
 value=Hodnota
 
+filter.is_archived=Archivované
+filter.is_template=Šablóna
+filter.private=Súkromný
+
+
+[search]
+
 [aria]
 navbar=Navigačná lišta
 footer=Päta
@@ -309,7 +316,6 @@ collaborative_repos=Kolaboratívne repozitáre
 my_orgs=Moje organizácie
 my_mirrors=Moje zrkadlá
 view_home=Zobraziť %s
-search_repos=Nájsť repozitár…
 filter=Ostatné filtre
 filter_by_team_repositories=Filtrovať podľa tímových repozitárov
 feed_of=Informačný kanál „%s“
@@ -330,20 +336,8 @@ issues.in_your_repos=Vo vašich repozitároch
 repos=Repozitáre
 users=Používatelia
 organizations=Organizácie
-search=Hľadať
 go_to=Ísť na
 code=Zdrojový kód
-search.type.tooltip=Typ vyhľadávania
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Zahrnúť iba výsledky, ktoré sa takmer zhodujú s hľadaným výrazom
-search.match=Zhoda
-search.match.tooltip=Zahrnúť iba výsledky, ktoré sa presne zhodujú s hľadaným výrazom
-code_search_unavailable=Vyhľadávanie kódu momentálne nie je dostupné. Kontaktujte, prosím, správcu.
-repo_no_results=Nenašli sa zodpovedajúce repozitáre.
-user_no_results=Nenašli sa zodpovedajúci používatelia.
-org_no_results=Nenašli sa zodpovedajúce organizácie.
-code_no_results=Nenašiel sa žiaden zdrojový kód zodpovedajúci hľadanému výrazu.
-code_search_results=`Výsledky hľadania pre "%s"`
 code_last_indexed_at=Naposledy indexované %s
 relevant_repositories_tooltip=Repozitáre, ktoré sú forkami alebo ktoré nemajú tému, žiadnu ikonu ani popis, sú skryté.
 relevant_repositories=Zobrazujú sa iba relevantné repozitáre, <a href="%s">zobraziť nefiltrované výsledky</a>.
@@ -359,7 +353,6 @@ remember_me=Zapamätať si toto zariadenie
 forgot_password_title=Zabudnuté heslo
 forgot_password=Zabudli ste heslo?
 sign_up_now=Potrebujete účet? Zaregistrujte sa teraz.
-confirmation_mail_sent_prompt=Na adresu <b>%s</b> bol odoslaný nový potvrdzovací e-mail. Skontrolujte si, prosím, vašu doručenú poštu počas najbližších %s pre dokončenie procesu registrácie.
 allow_password_change=Vyžiadať od používateľa zmenu hesla (doporučuje sa)
 reset_password_mail_sent_prompt=Na adresu <b>%s</b> bol odoslaný potvrdzovací e-mail. Skontrolujte si, prosím, vašu doručenú poštu počas najbližších %s pre dokončenie procesu obnovenia účtu.
 active_your_account=Aktivovať účet
@@ -563,6 +556,7 @@ auth_failed=Overenie zlyhalo: %v
 
 target_branch_not_exist=Cieľová vetva neexistuje.
 
+
 [user]
 change_avatar=Zmeniť svoj avatar…
 joined_on=Pripojil/a sa %s
@@ -581,6 +575,7 @@ user_bio=Životopis
 disabled_public_activity=Tento používateľ zákázal verejnú viditeľnosť aktivity.
 
 
+
 [settings]
 profile=Profil
 account=Účet
@@ -706,7 +701,6 @@ gpg_invalid_token_signature=Zadaný GPG kľúč, podpis a token sa nezhodujú al
 gpg_token_required=Musíte zadať podpis pre nižšie uvedený token
 gpg_token=Token
 gpg_token_help=Podpis môžete vygenerovať pomocou:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Zakódovaný (ASCII) podpis GPG
 key_signature_gpg_placeholder=Začína s '-----BEGIN PGP SIGNATURE-----'
 ssh_key_verified=Overený kľúč
@@ -852,7 +846,6 @@ visibility_helper_forced=Váš správca vynucuje že nové repozitáre musia by
 visibility_fork_helper=(Zmena ovplyvní všetky forky.)
 clone_helper=Potrebujete pomoc s klonovaním? Navštívte <a target="_blank" rel="noopener noreferrer" href="%s">Pomocníka</a>.
 use_template=Použiť túto šablónu
-clone_in_vsc=Klonovať vo VS Code
 generate_repo=Generovať repozitár
 generate_from=Generovať z
 repo_desc=Popis
@@ -1036,7 +1029,6 @@ editor.no_commit_to_branch=Nedá sa odoslať priamo do vetvy, pretože:
 editor.require_signed_commit=Vetva vyžaduje podpísaný commit
 
 commits.commits=Commity
-commits.find=Hľadať
 commits.search_all=Všetky vetvy
 commits.author=Autor
 commits.message=Správa
@@ -1144,14 +1136,7 @@ activity.unresolved_conv_label=Otvoriť
 activity.git_stats_commit_1=%d commit
 activity.git_stats_commit_n=%d commity
 
-search=Hľadať
-search.type.tooltip=Typ vyhľadávania
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Zahrnúť iba výsledky, ktoré sa takmer zhodujú s hľadaným výrazom
-search.match=Zhoda
-search.match.tooltip=Zahrnúť iba výsledky, ktoré sa presne zhodujú s hľadaným výrazom
-search.code_no_results=Nenašiel sa žiaden zdrojový kód zodpovedajúci hľadanému výrazu.
-search.code_search_unavailable=Vyhľadávanie kódu momentálne nie je dostupné. Kontaktujte, prosím, správcu.
+contributors.contribution_type.commits=Commitov
 
 settings.collaboration.owner=Vlastník
 settings.hooks=Webhooky
@@ -1246,6 +1231,8 @@ release.cancel=Zrušiť
 
 
 
+[graphs]
+
 [org]
 code=Kód
 lower_repositories=repozitáre
@@ -1282,7 +1269,6 @@ dashboard.delete_generated_repository_avatars=Odstrániť vygenerované avatary
 
 repos.owner=Vlastník
 repos.private=Súkromný
-repos.forks=Forky
 
 packages.owner=Vlastník
 packages.repository=Repozitár
@@ -1328,6 +1314,7 @@ monitor.process.cancel=Zrušiť proces
 
 
 
+
 [action]
 compare_commits=Porovnať %d commitov
 compare_commits_general=Porovnať commity
diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini
index 411a83ed75..9234e9aa58 100644
--- a/options/locale/locale_sv-SE.ini
+++ b/options/locale/locale_sv-SE.ini
@@ -91,6 +91,14 @@ concept_user_organization=Organisation
 
 name=Namn
 
+filter.is_archived=Arkiverade
+filter.is_template=Mall
+filter.public=Offentlig
+filter.private=Privat
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -212,7 +220,6 @@ collaborative_repos=Kollaborativa Utvecklingskataloger
 my_orgs=Mina organisationer
 my_mirrors=Mina speglar
 view_home=Visa %s
-search_repos=Hitta en utvecklingskatalog…
 filter=Övriga Filter
 
 show_archived=Arkiverade
@@ -231,12 +238,7 @@ issues.in_your_repos=I dina utvecklingskataloger
 repos=Utvecklingskataloger
 users=Användare
 organizations=Organisationer
-search=Sök
 code=Kod
-repo_no_results=Inga matchande utvecklingskataloger hittades.
-user_no_results=Inga matchande användare hittades.
-org_no_results=Inga matchande organisationer hittades.
-code_no_results=Ingen källkod hittades som matchar din sökterm.
 code_last_indexed_at=Indexerades senast %s
 
 [auth]
@@ -249,7 +251,6 @@ remember_me=Kom ihåg denna enhet
 forgot_password_title=Glömt lösenord
 forgot_password=Glömt lösenord?
 sign_up_now=Behöver du ett konto? Registrera nu.
-confirmation_mail_sent_prompt=Ett nytt bekräftelsemail has skickats till <b>%s</b>. Vänligen kolla din inkorg inom dom kommande %s för att slutföra registreringsprocessen.
 must_change_password=Ändra ditt lösenord
 allow_password_change=Kräv att användaren byter lösenord (rekommenderas)
 reset_password_mail_sent_prompt=Ett nytt bekräftelsemail has skickats till <b>%s</b>. Vänligen kontrollera din inkorg inom de kommande %s för att slutföra återställning av ditt konto.
@@ -390,6 +391,7 @@ auth_failed=Autentisering misslyckades: %v
 
 target_branch_not_exist=Målgrenen finns inte.
 
+
 [user]
 change_avatar=Byt din avatar…
 repositories=Utvecklingskataloger
@@ -405,6 +407,7 @@ user_bio=Biografi
 disabled_public_activity=Den här användaren har inaktiverat den publika synligheten av aktiviteten.
 
 
+
 [settings]
 profile=Profil
 account=Konto
@@ -797,8 +800,6 @@ editor.require_signed_commit=Branchen kräver en signerad commit
 
 commits.desc=Bläddra i källkodens förändringshistorik.
 commits.commits=Incheckningar
-commits.search=Sök commits…
-commits.find=Sök
 commits.search_all=Alla brancher
 commits.author=Upphovsman
 commits.message=Meddelande
@@ -826,7 +827,6 @@ projects.edit=Redigera projekt
 projects.modify=Uppdatera projekt
 projects.type.none=Ingen
 projects.template.desc=Projektmall
-projects.type.uncategorized=Okatergoriserad
 projects.column.edit_title=Namn
 projects.column.new_title=Namn
 projects.open=Öppna
@@ -1083,7 +1083,6 @@ pulls.compare_changes_desc=Välj branchen att merga in i, och ifrån.
 pulls.compare_base=merga in i
 pulls.compare_compare=pulla från
 pulls.filter_branch=Filtrera gren
-pulls.no_results=Inga resultat hittades.
 pulls.nothing_to_compare=Dessa brancher är ekvivalenta. Det finns ingen anledning att skapa en pull-request.
 pulls.create=Skapa Pullförfrågan
 pulls.title_desc=vill sammanfoga %[1]d incheckningar från <code>s[2]s</code> in i <code id="branch_target">%[3]s</code>
@@ -1227,10 +1226,7 @@ activity.git_stats_and_deletions=och
 activity.git_stats_deletion_1=%d borttagen
 activity.git_stats_deletion_n=%d borttagningar
 
-search=Sök
-search.search_repo=Sök utvecklingskatalog
-search.results=Sökresultat för ”%s” i <a href="%s"> %s</a>
-search.code_no_results=Ingen källkod hittades som matchar din sökterm.
+contributors.contribution_type.commits=Incheckningar
 
 settings=Inställningar
 settings.desc=Inställningarna är där du kan hantera inställningar för utvecklingskatalogen
@@ -1312,7 +1308,6 @@ settings.delete_collaborator=Ta bort
 settings.collaborator_deletion=Ta bort medarbetare
 settings.collaborator_deletion_desc=Borttagning av en medarbetare kommer att återkalla deras åtkomst till utvecklingskatalogen. Vill du fortsätta?
 settings.remove_collaborator_success=Medarbetaren har blivit borttagen.
-settings.search_user_placeholder=Sök användare…
 settings.org_not_allowed_to_be_collaborator=Organisationer kan inte läggas till som en medarbetare.
 settings.change_team_access_not_allowed=Att ändra teamåtkomst för utvecklingskatalogen har begränsats till organisationsägaren
 settings.team_not_in_organization=Teamet är inte i samma organisation som utvecklingskatalogen
@@ -1404,9 +1399,7 @@ settings.protect_enable_push=Aktivera Push
 settings.protect_enable_push_desc=Alla med skrivrättigheter kommer att kunna pusha till denna branch (men inte force-pusha).
 settings.protect_whitelist_deploy_keys=Vitlista deploy-nyckar med skrivåtkomst till push.
 settings.protect_whitelist_users=Vitlistade användare för pushning:
-settings.protect_whitelist_search_users=Sök användare…
 settings.protect_whitelist_teams=Vitlistade team för pushning:
-settings.protect_whitelist_search_teams=Sök team…
 settings.protect_merge_whitelist_committers=Aktivera vitlista för sammanfogning
 settings.protect_merge_whitelist_committers_desc=Tillåt endast vitlistade användare eller team att sammanfoga pull requests i denna branch.
 settings.protect_merge_whitelist_users=Vitlistade användare för sammanfogning:
@@ -1535,6 +1528,8 @@ topic.count_prompt=Du kan inte välja fler än 25 ämnen
 
 
 
+[graphs]
+
 [org]
 org_name_holder=Organisationsnamn
 org_full_name_holder=Organisationens Fullständiga Namn
@@ -1621,7 +1616,6 @@ teams.write_permission_desc=Medlemskap i detta team ger <strong>skrivrättighete
 teams.admin_permission_desc=Medlemskap i detta team ger <strong>administratörsrättigheter</strong>: medlemmar kan läsa, pusha och lägga till medarbetare till teamets utvecklingskataloger.
 teams.create_repo_permission_desc=Vidare så ger detta team <strong>Skapa utvecklingskatalog</strong> rättigheten: medlemmar can skapa nya utvecklingskataloger i organisationen.
 teams.repositories=Teamförråd
-teams.search_repo_placeholder=Sök utvecklingskatalog…
 teams.remove_all_repos_title=Ta bort alla utvecklingskataloger för teamet
 teams.remove_all_repos_desc=Detta kommer att ta bort alla utvecklingskataloger från teamet.
 teams.add_all_repos_title=Lägg till alla utvecklingskataloger
@@ -1644,6 +1638,8 @@ organizations=Organisationer
 repositories=Utvecklingskataloger
 authentication=Autentiseringskälla
 config=Konfiguration
+config_summary=Översikt
+config_settings=Inställningar
 notices=Systemaviseringar
 monitor=Övervakning
 first_page=Första
@@ -1651,7 +1647,6 @@ last_page=Sista
 total=Totalt: %d
 
 dashboard.statistic=Översikt
-dashboard.operations=Operationer för underhåll
 dashboard.system_status=Status
 dashboard.operation_name=Operationsnamn
 dashboard.operation_switch=Byt till
@@ -1745,9 +1740,6 @@ repos.repo_manage_panel=Utvecklingskatalogshantering
 repos.owner=Ägare
 repos.name=Namn
 repos.private=Privat
-repos.watches=Vakter
-repos.stars=Stjärnor
-repos.forks=Forkar
 repos.issues=Ärenden
 repos.size=Storlek
 
@@ -1813,7 +1805,6 @@ auths.tip.bitbucket=Registrera en ny OAuth konsument på https://bitbucket.org/a
 auths.tip.dropbox=Skapa en ny applikation på https://www.dropbox.com/developers/apps
 auths.tip.facebook=Registrera en ny appliaktion på https://developers.facebook.com/apps och lägg till produkten ”Facebook-inloggning”
 auths.tip.github=Registrera en ny OAuth applikation på https://github.com/settings/applications/new
-auths.tip.gitlab=Registrera en ny applikation på https://gitlab.com/profile/applications
 auths.tip.google_plus=Erhåll inloggningsuppgifter för OAuth2 från Google API-konsolen på https://console.developers.google.com/
 auths.tip.openid_connect=Använd OpenID Connect Discovery länken (<server>/.well-known/openid-configuration) för att ange slutpunkterna
 auths.tip.twitter=Gå till https://dev.twitter.com/app, skapa en applikation och försäkra att alternativet "Allow this application to be used to Sign in with Twitter" är aktiverat
@@ -1971,6 +1962,7 @@ notices.desc=Beskrivning
 notices.op=Op.
 notices.delete_success=Systemnotifikationer har blivit raderade.
 
+
 [action]
 create_repo=skapade utvecklingskatalog <a href="%s"> %s</a>
 rename_repo=döpte om utvecklingskalatogen från <code>%[1]s</code> till <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index dd7d1b066e..119e1ef150 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -17,6 +17,7 @@ template=Şablon
 language=Dil
 notifications=Bildirimler
 active_stopwatch=Etkin Zaman Takibi
+tracked_time_summary=Konu listesi süzgeçlerine dayanan takip edilen zamanın özeti
 create_new=Oluştur…
 user_profile_and_more=Profil ve Ayarlar…
 signed_in_as=Giriş yapan:
@@ -90,6 +91,7 @@ remove=Kaldır
 remove_all=Tümünü Kaldır
 remove_label_str=`"%s" öğesini kaldır`
 edit=Düzenle
+view=Görüntüle
 
 enabled=Aktifleştirilmiş
 disabled=Devre Dışı
@@ -97,6 +99,7 @@ locked=Kilitli
 
 copy=Kopyala
 copy_url=URL'yi kopyala
+copy_hash=Hash'i kopyala
 copy_content=İçeriği kopyala
 copy_branch=Dal adını kopyala
 copy_success=Kopyalandı!
@@ -109,6 +112,7 @@ loading=Yükleniyor…
 
 error=Hata
 error404=Ulaşmaya çalıştığınız sayfa <strong>mevcut değil</strong> veya <strong>görüntüleme yetkiniz yok</strong>.
+go_back=Geri Git
 
 never=Asla
 unknown=Bilinmiyor
@@ -137,6 +141,15 @@ confirm_delete_selected=Tüm seçili öğeleri gerçekten silmek istiyor musunuz
 name=İsim
 value=Değer
 
+filter=Filtre
+filter.is_archived=Arşivlenmiş
+filter.is_template=Şablon
+filter.public=Genel
+filter.private=Özel
+
+
+[search]
+
 [aria]
 navbar=Gezinti Çubuğu
 footer=Alt Bilgi
@@ -180,6 +193,7 @@ network_error=Ağ hatası
 [startpage]
 app_desc=Zahmetsiz, kendi sunucunuzda barındırabileceğiniz Git servisi
 install=Kurulumu kolay
+install_desc=Platformunuz için <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">ikili dosyayı çalıştırın</a>, <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a> ile yükleyin veya <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">paket</a> olarak edinin.
 platform=Farklı platformlarda çalışablir
 platform_desc=Gitea <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> ile derleme yapılabilecek her yerde çalışmaktadır: Windows, macOS, Linux, ARM, vb. Hangisini seviyorsanız onu seçin!
 lightweight=Hafif
@@ -309,7 +323,6 @@ collaborative_repos=Katkıya Açık Depolar
 my_orgs=Organizasyonlarım
 my_mirrors=Yansılarım
 view_home=%s Görüntüle
-search_repos=Depo bul…
 filter=Diğer Süzgeçler
 filter_by_team_repositories=Takım depolarına göre süz
 feed_of=`"%s" beslemesi`
@@ -330,20 +343,8 @@ issues.in_your_repos=Depolarınızda
 repos=Depolar
 users=Kullanıcılar
 organizations=Organizasyonlar
-search=Ara
 go_to=Git
 code=Kod
-search.type.tooltip=Arama türü
-search.fuzzy=Bulanık
-search.fuzzy.tooltip=Arama terimine benzeyen sonuçları da içer
-search.match=Eşleştir
-search.match.tooltip=Sadece arama terimiyle tamamen eşleşen sonuçları içer
-code_search_unavailable=Kod arama şu an mevcut değil. Lütfen site yöneticinizle bağlantıya geçin.
-repo_no_results=Eşleşen depo bulunamadı.
-user_no_results=Eşleşen kullanıcı bulunamadı.
-org_no_results=Eşleşen organizasyon bulunamadı.
-code_no_results=Arama teriminizi içeren kaynak kod bulunamadı.
-code_search_results=`"%s" için sonuçları ara`
 code_last_indexed_at=Son dizinlenen %s
 relevant_repositories_tooltip=Çatal olan veya konusu, simgesi veya açıklaması olmayan depolar gizlenmiştir.
 relevant_repositories=Sadece ilişkili depolar gösteriliyor, <a href="%s">süzülmemiş sonuçları göster</a>.
@@ -356,11 +357,11 @@ disable_register_prompt=Kayıt işlemi devre dışıdır. Lütfen site yönetici
 disable_register_mail=Kayıt için e-posta doğrulama devre dışıdır.
 manual_activation_only=Etkinleştirmeyi tamamlamak için site yöneticinizle bağlantıya geçin.
 remember_me=Bu Aygıtı hatırla
+remember_me.compromised=Oturum açma tokeni artık geçerli değil, bu ele geçirilmiş bir hesaba işaret ediyor olabilir. Lütfen hesabınızda olağandışı faaliyet olup olmadığını denetleyin.
 forgot_password_title=Şifremi unuttum
 forgot_password=Şifrenizi mi unuttunuz?
 sign_up_now=Bir hesaba mı ihtiyacınız var? Hemen kaydolun.
 sign_up_successful=Hesap başarılı bir şekilde oluşturuldu. Hoşgeldiniz!
-confirmation_mail_sent_prompt=Yeni onay e-postası <b>%s</b> adresine gönderildi. Lütfen gelen kutunuzu bir sonraki %s e kadar kontrol edip kayıt işlemini tamamlayın.
 must_change_password=Parolanızı güncelleyin
 allow_password_change=Kullanıcıyı parola değiştirmeye zorla (önerilen)
 reset_password_mail_sent_prompt=<b>%s</b> adresine bir onay e-postası gönderildi. Hesap kurtarma işlemini tamamlamak için lütfen gelen kutunuzu sonraki %s içinde kontrol edin.
@@ -375,6 +376,7 @@ email_not_associate=Bu e-posta adresi hiçbir hesap ile ilişkilendirilmemiştir
 send_reset_mail=Hesap Kurtarma E-postası Gönder
 reset_password=Hesap Kurtarma
 invalid_code=Doğrulama kodunuz geçersiz veya süresi dolmuş.
+invalid_code_forgot_password=Onay kodunuz hatalı veya süresi geçmiş. Yeni bir oturum başlatmak için <a href="%s">buraya</a> tıklayın.
 invalid_password=Parolanız hesap oluşturulurken kullanılan parolayla eşleşmiyor.
 reset_password_helper=Hesabı Kurtar
 reset_password_wrong_user=%s olarak oturum açmışsınız, ancak hesap kurtarma bağlantısı %s için
@@ -581,6 +583,7 @@ org_still_own_packages=Bu organizasyon hala bir veya daha fazla pakete sahip, ö
 
 target_branch_not_exist=Hedef dal mevcut değil.
 
+
 [user]
 change_avatar=Profil resmini değiştir…
 joined_on=%s tarihinde katıldı
@@ -606,6 +609,7 @@ form.name_reserved=`"%s" kullanıcı adı rezerve edilmiş.`
 form.name_pattern_not_allowed=Kullanıcı adında "%s" deseni kullanılamaz.
 form.name_chars_not_allowed=`"%s" kullanıcı adı geçersiz karakterler içeriyor.`
 
+
 [settings]
 profile=Profil
 account=Hesap
@@ -676,6 +680,7 @@ choose_new_avatar=Yeni Avatar Seç
 update_avatar=Profil Resmini Güncelle
 delete_current_avatar=Güncel Avatarı Sil
 uploaded_avatar_not_a_image=Yüklenen dosya bir resim dosyası değil.
+uploaded_avatar_is_too_big=Yüklenen dosyanın boyutu (%d KiB), azami boyutu (%d KiB) aşıyor.
 update_avatar_success=Profil resminiz değiştirildi.
 update_user_avatar_success=Kullanıcının avatarı güncellendi.
 
@@ -749,7 +754,6 @@ gpg_invalid_token_signature=Verilen GPG anahtarı, imza ve anahtar uyuşmuyor ve
 gpg_token_required=Aşağıdaki anahtar için bir imza sağlamalısınız
 gpg_token=Anahtar
 gpg_token_help=Şunu kullanarak bir imza oluşturabilirsiniz:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Korumalı GPG imzası
 key_signature_gpg_placeholder='-----PGP İMZA BAŞLAT -----' ile başlar
 verify_gpg_key_success=GPG anahtarı "%s" doğrulandı.
@@ -857,6 +861,7 @@ revoke_oauth2_grant_description=Bu üçüncü taraf uygulamasına erişimin ipta
 revoke_oauth2_grant_success=Erişim başarıyla kaldırıldı.
 
 twofa_desc=İki faktörlü kimlik doğrulama, hesabınızın güvenliğini artırır.
+twofa_recovery_tip=Aygıtınızı kaybetmeniz durumunda, hesabınıza tekrar erişmek için tek kullanımlık kurtarma anahtarını kullanabileceksiniz.
 twofa_is_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde <strong>kaydedilmiş</strong>.
 twofa_not_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde kaydedilmemiş.
 twofa_disable=İki Aşamalı Doğrulamayı Devre Dışı Bırak
@@ -879,6 +884,8 @@ webauthn_register_key=Güvenlik Anahtarı Ekle
 webauthn_nickname=Takma Ad
 webauthn_delete_key=Güvenlik Anahtarını Kaldır
 webauthn_delete_key_desc=Bir güvenlik anahtarını kaldırırsanız, onunla artık giriş yapamazsınız. Devam edilsin mi?
+webauthn_key_loss_warning=Güvenlik anahtarlarınızı kaybederseniz, hesabınıza erişimi kaybedersiniz.
+webauthn_alternative_tip=Ek bir kimlik doğrulama yöntemi ayarlamak isteyebilirsiniz.
 
 manage_account_links=Bağlı Hesapları Yönet
 manage_account_links_desc=Bu harici hesaplar Gitea hesabınızla bağlantılı.
@@ -915,6 +922,7 @@ visibility.private=Özel
 visibility.private_tooltip=Sadece katıldığınız organizasyonların üyeleri tarafından görünür
 
 [repo]
+new_repo_helper=Bir depo, sürüm geçmişi dahil tüm proje dosyalarını içerir. Zaten başka bir yerde mi barındırıyorsunuz? <a href="%s">Depoyu taşıyın.</a>
 owner=Sahibi
 owner_helper=Bazı organizasyonlar, en çok depo sayısı sınırı nedeniyle açılır menüde görünmeyebilir.
 repo_name=Depo İsmi
@@ -935,9 +943,10 @@ fork_from=Buradan Çatalla
 already_forked=%s deposunu zaten çatalladınız
 fork_to_different_account=Başka bir hesaba çatalla
 fork_visibility_helper=Çatallanmış bir deponun görünürlüğü değiştirilemez.
+fork_branch=Çatala klonlanacak dal
+all_branches=Tüm dallar
 fork_no_valid_owners=Geçerli bir sahibi olmadığı için bu depo çatallanamaz.
 use_template=Bu şablonu kullan
-clone_in_vsc=VS Code'ta klonla
 download_zip=ZIP indir
 download_tar=TAR.GZ indir
 download_bundle=BUNDLE indir
@@ -958,12 +967,13 @@ readme_helper=Bir README dosyası şablonu seçin.
 readme_helper_desc=Projeniz için eksiksiz bir açıklama yazabileceğiniz yer burasıdır.
 auto_init=Depoyu başlat (.gitignore, Lisans ve README dosyalarını ekler)
 trust_model_helper=İmza doğrulaması için güven modelini seçin. Olası seçenekler şunlardır:
-trust_model_helper_collaborator=Ortak çalışan: Ortak çalışanların imzalarına güven
+trust_model_helper_collaborator=Katkıcı: Katkıcıların imzalarına güven
 trust_model_helper_committer=İşleyen: İşleyenlerle eşleşen imzalara güven
-trust_model_helper_collaborator_committer=Ortak çalışan+İşleyen: İşleyenle eşleşen ortak çalışanların imzalarına güven
+trust_model_helper_collaborator_committer=Katkıcı+İşleyen: İşleyenle eşleşen ortak çalışanların imzalarına güven
 trust_model_helper_default=Varsayılan: Bu kurulum için varsayılan güven modelini kullan
 create_repo=Depo Oluştur
 default_branch=Varsayılan Dal
+default_branch_label=varsayılan
 default_branch_helper=Varsayılan dal, değişiklik istekleri ve kod işlemeleri için temel daldır.
 mirror_prune=Buda
 mirror_prune_desc=Kullanılmayan uzak depoları izleyen referansları kaldır
@@ -999,8 +1009,13 @@ delete_preexisting=Önceden var olan dosyaları sil
 delete_preexisting_content=%s içindeki dosyaları sil
 delete_preexisting_success=%s içindeki kabul edilmeyen dosyalar silindi
 blame_prior=Bu değişiklikten önceki suçu görüntüle
+blame.ignore_revs=<a href="%s">.git-blame-ignore-revs</a> dosyasındaki sürümler yok sayılıyor. Bunun yerine normal sorumlu görüntüsü için <a href="%s">buraya tıklayın</a>.
+blame.ignore_revs.failed=<a href="%s">.git-blame-ignore-revs</a> dosyasındaki sürümler yok sayılamadı.
 author_search_tooltip=En fazla 30 kullanıcı görüntüler
 
+tree_path_not_found_commit=%[1] yolu, %[2]s işlemesinde mevcut değil
+tree_path_not_found_branch=%[1] yolu, %[2]s dalında mevcut değil
+tree_path_not_found_tag=%[1] yolu, %[2]s etiketinde mevcut değil
 
 transfer.accept=Aktarımı Kabul Et
 transfer.accept_desc=`"%s" tarafına aktar`
@@ -1250,9 +1265,7 @@ commits.desc=Kaynak kodu değişiklik geçmişine göz atın.
 commits.commits=İşleme
 commits.no_commits=Ortak bir işleme yok. "%s" ve "%s" tamamen farklı geçmişlere sahip.
 commits.nothing_to_compare=Bu dallar eşit.
-commits.search=İşlemeleri ara…
 commits.search.tooltip=Anahtar kelimeleri "author:", "committer:", "after:" veya "before:" ile kullanabilirsiniz, örneğin "revert author:Alice before:2019-01-13".
-commits.find=Ara
 commits.search_all=Tüm Dallar
 commits.author=Yazar
 commits.message=Mesaj
@@ -1264,6 +1277,7 @@ commits.signed_by_untrusted_user=Güvenilmeyen kullanıcı tarafından imzaland
 commits.signed_by_untrusted_user_unmatched=İşleyici ile eşleşmeyen güvenilmeyen kullanıcı tarafından imzalanmış
 commits.gpg_key_id=GPG Anahtar Kimliği
 commits.ssh_key_fingerprint=SSH Anahtar Parmak İzi
+commits.view_path=Geçmişte bu noktayı görüntüle
 
 commit.operations=İşlemler
 commit.revert=Geri Al
@@ -1302,7 +1316,6 @@ projects.type.basic_kanban=Kanban Tabanı
 projects.type.bug_triage=Hata Triyajı
 projects.template.desc=Proje şablonu
 projects.template.desc_helper=Başlamak için bir proje şablonu seçin
-projects.type.uncategorized=Kategorize edilmemiş
 projects.column.edit=Sütun Düzenle
 projects.column.edit_title=İsim
 projects.column.new_title=İsim
@@ -1310,10 +1323,7 @@ projects.column.new_submit=Sütun Oluştur
 projects.column.new=Yeni Sütun
 projects.column.set_default=Varsayılanı Ayarla
 projects.column.set_default_desc=Bu sütunu kategorize edilmemiş konular ve değişiklik istekleri için varsayılan olarak ayarlayın
-projects.column.unset_default=Varsayılanları Geri Al
-projects.column.unset_default_desc=Bu sütunu varsayılan olarak geri al
 projects.column.delete=Sutün Sil
-projects.column.deletion_desc=Bir proje sütununun silinmesi, ilgili tüm konuları 'Kategorize edilmemiş'e taşır. Devam edilsin mi?
 projects.column.color=Renk
 projects.open=Aç
 projects.close=Kapat
@@ -1425,7 +1435,6 @@ issues.filter_sort.moststars=En çok yıldızlılar
 issues.filter_sort.feweststars=En az yıldızlılar
 issues.filter_sort.mostforks=En çok çatallananlar
 issues.filter_sort.fewestforks=En az çatallananlar
-issues.keyword_search_unavailable=Anahtar kelime ile arama şu an mevcut değil. Lütfen site yöneticisiyle iletişime geçin.
 issues.action_open=Açık
 issues.action_close=Kapat
 issues.action_label=Etiket
@@ -1474,8 +1483,17 @@ issues.ref_closed_from=`<a href="%[3]s">bu konuyu kapat%[4]s</a> <a id="%[1]s" h
 issues.ref_reopened_from=`<a href="%[3]s">konuyu yeniden aç%[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.ref_from=`%[1]s'den`
 issues.author=Yazar
+issues.author_helper=Bu kullanıcı yazardır.
 issues.role.owner=Sahibi
+issues.role.owner_helper=Bu kullanıcı bu deponun sahibidir.
 issues.role.member=Üye
+issues.role.member_helper=Bu kullanıcı bu deponun sahibi olan organizasyonun üyesidir.
+issues.role.collaborator=Katkıcı
+issues.role.collaborator_helper=Kullanıcı bu depoya işbirliği için davet edildi.
+issues.role.first_time_contributor=İlk defa katkıcı
+issues.role.first_time_contributor_helper=Bu, bu kullanıcının bu depoya ilk katkısı.
+issues.role.contributor=Katılımcı
+issues.role.contributor_helper=Bu kullanıcı bu depoya daha önce işleme gönderdi.
 issues.re_request_review=İncelemeyi yeniden iste
 issues.is_stale=Bu incelemeden bu yana bu istekte değişiklikler oldu
 issues.remove_request_review=İnceleme isteğini kaldır
@@ -1491,6 +1509,8 @@ issues.label_description=Etiket açıklaması
 issues.label_color=Etiket rengi
 issues.label_exclusive=Özel
 issues.label_archive=Etiketi Arşivle
+issues.label_archived_filter=Arşivlenmiş etiketleri göster
+issues.label_archive_tooltip=Arşivlenmiş etiketler, etiket araması yapılırken varsayılan olarak önerilerin dışında tutuluyor.
 issues.label_exclusive_desc=<code>Kapsam/öğe</code> etiketini, diğer <code>kapsam/</code> etiketleriyle ayrışık olacak şekilde adlandırın.
 issues.label_exclusive_warning=Çakışan kapsamlı etiketler, bir konu veya değişiklik isteği etiketleri düzenlenirken kaldırılacaktır.
 issues.label_count=%d etiket
@@ -1666,7 +1686,6 @@ pulls.compare_compare=şuradan çek
 pulls.switch_comparison_type=Karşılaştırma türünü değiştir
 pulls.switch_head_and_base=Ana ve temeli değiştir
 pulls.filter_branch=Dal filtrele
-pulls.no_results=Sonuç bulunamadı.
 pulls.show_all_commits=Tüm işlemeleri göster
 pulls.show_changes_since_your_last_review=Son incelemenizden sonraki değişiklikleri göster
 pulls.showing_only_single_commit=Sadece %[1]s işlemesindeki değişiklikler gösteriliyor
@@ -1745,6 +1764,7 @@ pulls.rebase_conflict_summary=Hata Mesajı
 pulls.unrelated_histories=Birleştirme Başarısız: Birleştirme başlığı ve tabanı ortak bir geçmişi paylaşmıyor. İpucu: Farklı bir strateji deneyin
 pulls.merge_out_of_date=Birleştirme Başarısız: Birleştirme oluşturulurken, taban güncellendi. İpucu: Tekrar deneyin.
 pulls.head_out_of_date=Birleştirme Başarısız: Birleştirme oluşturulurken, ana güncellendi. İpucu: Tekrar deneyin.
+pulls.has_merged=Başarısız: Değişiklik isteği birleştirildi, yeniden birleştiremez veya hedef dalı değiştiremezsiniz.
 pulls.push_rejected=Birleştirme Başarısız Oldu: Gönderme reddedildi. Bu depo için Git İstemcilerini inceleyin.
 pulls.push_rejected_summary=Tam Red Mesajı
 pulls.push_rejected_no_message=Birleştirme başarısız oldu: Gönderme reddedildi, ancak uzak bir mesaj yoktu.<br>Bu depo için Git İstemcilerini inceleyin
@@ -1756,6 +1776,8 @@ pulls.status_checks_failure=Bazı kontroller başarısız oldu
 pulls.status_checks_error=Bazı kontroller hatalar bildirdi
 pulls.status_checks_requested=Gerekli
 pulls.status_checks_details=Ayrıntılar
+pulls.status_checks_hide_all=Tüm denetlemeleri gizle
+pulls.status_checks_show_all=Tüm denetlemeleri göster
 pulls.update_branch=Dalı birleştirmeyle güncelle
 pulls.update_branch_rebase=Dalı yeniden yapılandırmayla güncelle
 pulls.update_branch_success=Dal güncellemesi başarıyla gerçekleştirildi
@@ -1764,6 +1786,11 @@ pulls.outdated_with_base_branch=Bu dal, temel dal ile güncel değil
 pulls.close=Değişiklik İsteğini Kapat
 pulls.closed_at=`<a id="%[1]s" href="#%[1]s">%[2]s</a> değişiklik isteğini kapattı`
 pulls.reopened_at=`<a id="%[1]s" href="#%[1]s">%[2]s</a> değişiklik isteğini yeniden açtı`
+pulls.cmd_instruction_hint=`<a class="show-instruction">Komut satırı talimatlarını</a> görüntüleyin.`
+pulls.cmd_instruction_checkout_title=Çekme
+pulls.cmd_instruction_checkout_desc=Proje deponuzdan yeni bir dalı çekin ve değişiklikleri test edin.
+pulls.cmd_instruction_merge_title=Birleştir
+pulls.cmd_instruction_merge_desc=Değişiklikleri birleştirin ve Gitea'da güncelleyin.
 pulls.clear_merge_message=Birleştirme iletilerini temizle
 pulls.clear_merge_message_hint=Birleştirme iletisini temizlemek sadece işleme ileti içeriğini kaldırır ama üretilmiş "Co-Authored-By …" gibi git fragmanlarını korur.
 
@@ -1809,6 +1836,8 @@ milestones.edit_success=`"%s" dönüm noktası güncellendi.`
 milestones.deletion=Kilometre Taşını Sil
 milestones.deletion_desc=Bir kilometre taşını silmek, onu ilgili tüm sorunlardan kaldırır. Devam edilsin mi?
 milestones.deletion_success=Kilometre taşı silindi.
+milestones.filter_sort.earliest_due_data=En erken bitiş tarihi
+milestones.filter_sort.latest_due_date=En uzak bitiş tarihi
 milestones.filter_sort.least_complete=En az tamamlama
 milestones.filter_sort.most_complete=En çok tamamlama
 milestones.filter_sort.most_issues=En çok konu
@@ -1924,16 +1953,7 @@ activity.git_stats_and_deletions=ve
 activity.git_stats_deletion_1=%d silme oldu
 activity.git_stats_deletion_n=%d silme oldu
 
-search=Ara
-search.search_repo=Depo ara
-search.type.tooltip=Arama türü
-search.fuzzy=Belirsiz
-search.fuzzy.tooltip=Arama terimine benzeyen sonuçları da içer
-search.match=Eşleştir
-search.match.tooltip=Sadece arama terimiyle tamamen eşleşen sonuçları içer
-search.results=`"%s" için <a href="%s">%s</a> içinde sonuçları ara`
-search.code_no_results=Arama teriminizi içeren kaynak kod bulunamadı.
-search.code_search_unavailable=Kod arama şu an mevcut değil. Lütfen site yöneticinizle bağlantıya geçin.
+contributors.contribution_type.commits=İşleme
 
 settings=Ayarlar
 settings.desc=Ayarlar, deponun ayarlarını yönetebileceğiniz yerdir
@@ -1971,6 +1991,8 @@ settings.mirror_settings.push_mirror.add=Yansı Gönderimi Ekle
 settings.mirror_settings.push_mirror.edit_sync_time=Yansı eşzamanlama aralığını düzenle
 
 settings.sync_mirror=Şimdi Eşitle
+settings.pull_mirror_sync_in_progress=Şu an %s uzak sunucusundan değişiklikler çekiliyor.
+settings.push_mirror_sync_in_progress=Şu an %s uzak sunucusuna değişiklikler itiliyor.
 settings.site=Web Sitesi
 settings.update_settings=Ayarları Güncelle
 settings.update_mirror_settings=Yansı Ayarları Güncelle
@@ -2010,6 +2032,7 @@ settings.pulls.default_allow_edits_from_maintainers=Bakımcıların düzenlemele
 settings.releases_desc=Depo Sürümlerini Etkinleştir
 settings.packages_desc=Depo Paket Kütüğünü Etkinleştir
 settings.projects_desc=Depo Projelerini Etkinleştir
+settings.projects_mode_all=Tüm projeler
 settings.actions_desc=Depo İşlemlerini Etkinleştir
 settings.admin_settings=Yönetici Ayarları
 settings.admin_enable_health_check=Depo Sağlık Kontrollerini Etkinleştir (git fsck)
@@ -2084,7 +2107,6 @@ settings.delete_collaborator=Sil
 settings.collaborator_deletion=Katkıcıyı Sil
 settings.collaborator_deletion_desc=Bir katkıcıyı silmek, bu depoya erişimini iptal edecektir. Devam et?
 settings.remove_collaborator_success=Katkıcı silindi.
-settings.search_user_placeholder=Kullanıcı ara…
 settings.org_not_allowed_to_be_collaborator=Organizasyonlar katkıcı olarak eklenemez.
 settings.change_team_access_not_allowed=Depo için takım erişimini değiştirmek, organizasyon sahibiyle sınırlandırıldı
 settings.team_not_in_organization=Takım, depo ile aynı organizasyonda değil
@@ -2092,7 +2114,6 @@ settings.teams=Takımlar
 settings.add_team=Takım Ekle
 settings.add_team_duplicate=Takım zaten bu depoya sahip
 settings.add_team_success=Takım artık bu depoya erişebilir.
-settings.search_team=Takım Ara…
 settings.change_team_permission_tip=Takımın izni takım ayarı sayfasında ayarlanır ve depo başına değiştirilemez
 settings.delete_team_tip=Bu takımın tüm depolara erişimi var ve kaldırılamıyor
 settings.remove_team_success=Takımın depoya erişimi kaldırıldı.
@@ -2104,12 +2125,14 @@ settings.webhook_deletion_desc=Bir web isteğini kaldırmak, ayarlarını ve tes
 settings.webhook_deletion_success=Web isteği silindi.
 settings.webhook.test_delivery=Test Dağıtımı
 settings.webhook.test_delivery_desc=Bu web isteğini sahte bir olayla test edin.
+settings.webhook.test_delivery_desc_disabled=Bu web istemcisini sahte bir olayla denemek için etkinleştirin.
 settings.webhook.request=İstekler
 settings.webhook.response=Cevaplar
 settings.webhook.headers=Başlıklar
 settings.webhook.payload=İçerik
 settings.webhook.body=Gövde
 settings.webhook.replay.description=Bu web kancasını tekrar çalıştır.
+settings.webhook.replay.description_disabled=Bu web istemcisini yeniden oynatmak için etkinleştirin.
 settings.webhook.delivery.success=Teslim kuyruğuna bir olay eklendi. Teslim geçmişinde görünmesi birkaç saniye alabilir.
 settings.githooks_desc=Git İstemcileri Git'in kendisi tarafından desteklenmektedir. Özel işlemler ayarlamak için aşağıdaki istemci dosyalarını düzenleyebilirsiniz.
 settings.githook_edit_desc=İstek aktif değilse örnek içerik sunulacaktır. İçeriği boş bırakmak, isteği devre dışı bırakmayı beraberinde getirecektir.
@@ -2243,9 +2266,7 @@ settings.protect_whitelist_committers=Beyaz Liste Kısıtlı Gönderme
 settings.protect_whitelist_committers_desc=Sadece beyaz listeye alınmış kullanıcıların veya takımların bu dala göndermesine izin verilir (ancak zorla gönderim yapmayın).
 settings.protect_whitelist_deploy_keys=Beyaz liste göndermek için yazma erişimi olan anahtarları dağıtır.
 settings.protect_whitelist_users=İtme için beyaz listedeki kullanıcılar:
-settings.protect_whitelist_search_users=Kullanıcı ara…
 settings.protect_whitelist_teams=İtme için beyaz listedeki takımlar:
-settings.protect_whitelist_search_teams=Takımları ara…
 settings.protect_merge_whitelist_committers=Birleştirme Beyaz Listesini Etkinleştir
 settings.protect_merge_whitelist_committers_desc=Yalnızca beyaz listedeki kullanıcıların veya takımların bu daldaki değişiklik isteklerini birleştirmesine izin verin.
 settings.protect_merge_whitelist_users=Birleştirme için beyaz listedeki kullanıcılar:
@@ -2269,6 +2290,7 @@ settings.dismiss_stale_approvals_desc=Değişiklik isteğinin içeriğini deği
 settings.require_signed_commits=İmzalı İşleme Gerekli
 settings.require_signed_commits_desc=Reddetme, onlar imzasızsa veya doğrulanamazsa bu dala gönderir.
 settings.protect_branch_name_pattern=Korunmuş Dal Adı Deseni
+settings.protect_branch_name_pattern_desc=Korunmuş dal isim desenleri. Desen sözdizimi için <a href="https://github.com/gobwas/glob">belgelere</a> bakabilirsiniz. Örnekler: main, release/**
 settings.protect_patterns=Desenler
 settings.protect_protected_file_patterns=Korumalı dosya kalıpları (noktalı virgülle ayrılmış ';'):
 settings.protect_protected_file_patterns_desc=Kullanıcının bu dalda dosya ekleme, düzenleme veya silme hakları olsa bile doğrudan değiştirilmesine izin verilmeyen korumalı dosyalar. Birden çok desen noktalı virgül (';') kullanılarak ayrılabilir. Desen sözdizimi için <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> belgelerine bakın. Örnekler: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
@@ -2305,6 +2327,7 @@ settings.tags.protection.allowed.teams=İzin verilen takımlar
 settings.tags.protection.allowed.noone=Hiç kimse
 settings.tags.protection.create=Etiketi Koru
 settings.tags.protection.none=Korumalı etiket yok.
+settings.tags.protection.pattern.description=Birden çok etiketi eşleştirmek için tek bir ad, glob deseni veya normal ifade kullanabilirsiniz. Daha fazlası için <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">korumalı etiketler rehberini</a> okuyun.
 settings.bot_token=Bot Jetonu
 settings.chat_id=Sohbet Kimliği
 settings.thread_id=İş Parçacığı ID
@@ -2516,6 +2539,8 @@ error.csv.too_large=Bu dosya çok büyük olduğu için işlenemiyor.
 error.csv.unexpected=%d satırı ve %d sütununda beklenmeyen bir karakter içerdiğinden bu dosya işlenemiyor.
 error.csv.invalid_field_count=%d satırında yanlış sayıda alan olduğundan bu dosya işlenemiyor.
 
+[graphs]
+
 [org]
 org_name_holder=Organizasyon Adı
 org_full_name_holder=Organizasyon Tam Adı
@@ -2620,7 +2645,6 @@ teams.write_permission_desc=Bu takım <strong>Yazma</strong> erişimi veriyor. 
 teams.admin_permission_desc=Bu takım <strong>Yönetici</strong> erişimi veriyor. Üyeler takım depolarını okuyabilir, itebilir ve katkıcı ekleyebilir.
 teams.create_repo_permission_desc=Ayrıca, bu takım <strong>Depo oluşturma</strong> izni verir: üyeler organizasyonda yeni depolar oluşturabilir.
 teams.repositories=Takım Depoları
-teams.search_repo_placeholder=Depo ara…
 teams.remove_all_repos_title=Tüm takım depolarını kaldır
 teams.remove_all_repos_desc=Bu, tüm depoları takımdan kaldıracaktır.
 teams.add_all_repos_title=Tüm depoları ekle
@@ -2652,6 +2676,8 @@ integrations=Bütünleştirmeler
 authentication=Yetkilendirme Kaynakları
 emails=Kullanıcı E-postaları
 config=Yapılandırma
+config_summary=Özet
+config_settings=Ayarlar
 notices=Sistem Bildirimler
 monitor=İzleme
 first_page=İlk
@@ -2661,7 +2687,6 @@ settings=Yönetici Ayarları
 
 dashboard.new_version_hint=Gitea %s şimdi hazır, %s çalıştırıyorsunuz. Ayrıntılar için <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blog</a>'a bakabilirsiniz.
 dashboard.statistic=Özet
-dashboard.operations=Bakım İşlemleri
 dashboard.system_status=Sistem Durumu
 dashboard.operation_name=İşlem Adı
 dashboard.operation_switch=Geç
@@ -2701,6 +2726,7 @@ dashboard.reinit_missing_repos=Kayıtları bulunanlar için tüm eksik Git depol
 dashboard.sync_external_users=Harici kullanıcı verisini senkronize et
 dashboard.cleanup_hook_task_table=Hook_task tablosunu temizleme
 dashboard.cleanup_packages=Süresi dolmuş paketleri temizleme
+dashboard.cleanup_actions=Eylemlerin süresi geçmiş günlük ve yapılarını temizle
 dashboard.server_uptime=Sunucunun Ayakta Kalma Süresi
 dashboard.current_goroutine=Güncel Goroutine'ler
 dashboard.current_memory_usage=Güncel Bellek Kullanımı
@@ -2738,7 +2764,9 @@ dashboard.gc_lfs=LFS üst nesnelerin atıklarını temizle
 dashboard.stop_zombie_tasks=Zombi görevleri durdur
 dashboard.stop_endless_tasks=Daimi görevleri durdur
 dashboard.cancel_abandoned_jobs=Terkedilmiş görevleri iptal et
+dashboard.start_schedule_tasks=Zamanlanmış görevleri başlat
 dashboard.sync_branch.started=Dal Eşzamanlaması başladı
+dashboard.rebuild_issue_indexer=Konu indeksini yeniden oluştur
 
 users.user_manage_panel=Kullanıcı Hesap Yönetimi
 users.new_account=Yeni Kullanıcı Hesabı
@@ -2747,6 +2775,9 @@ users.full_name=Tam İsim
 users.activated=Aktifleştirilmiş
 users.admin=Yönetici
 users.restricted=Kısıtlanmış
+users.reserved=Rezerve
+users.bot=Bot
+users.remote=Uzak
 users.2fa=2FD
 users.repos=Depolar
 users.created=Oluşturuldu
@@ -2793,6 +2824,7 @@ users.list_status_filter.is_prohibit_login=Oturum Açmayı Önle
 users.list_status_filter.not_prohibit_login=Oturum Açmaya İzin Ver
 users.list_status_filter.is_2fa_enabled=2FA Etkin
 users.list_status_filter.not_2fa_enabled=2FA Devre Dışı
+users.details=Kullanıcı Ayrıntıları
 
 emails.email_manage_panel=Kullanıcı E-posta Yönetimi
 emails.primary=Birincil
@@ -2805,6 +2837,7 @@ emails.updated=E-posta güncellendi
 emails.not_updated=İstenen e-posta adresi güncellenemedi: %v
 emails.duplicate_active=Bu e-posta adresi farklı bir kullanıcı için zaten aktif.
 emails.change_email_header=E-posta Özelliklerini Güncelle
+emails.change_email_text=Bu e-posta adresini güncellemek istediğinizden emin misiniz?
 
 orgs.org_manage_panel=Organizasyon Yönetimi
 orgs.name=İsim
@@ -2818,9 +2851,6 @@ repos.unadopted.no_more=Kabul edilmemiş başka depo bulunamadı
 repos.owner=Sahibi
 repos.name=İsim
 repos.private=Özel
-repos.watches=İzlemeler
-repos.stars=Yıldızlar
-repos.forks=Çatallar
 repos.issues=Konular
 repos.size=Boyut
 repos.lfs_size=LFS Boyutu
@@ -2829,6 +2859,7 @@ packages.package_manage_panel=Paket Yönetimi
 packages.total_size=Toplam Boyut: %s
 packages.unreferenced_size=Referanssız Boyut: %s
 packages.cleanup=Süresi dolmuş veriyi temizle
+packages.cleanup.success=Süresi dolmuş veri başarıyla temizlendi
 packages.owner=Sahibi
 packages.creator=Oluşturan
 packages.name=İsim
@@ -2839,10 +2870,12 @@ packages.size=Boyut
 packages.published=Yayınlandı
 
 defaulthooks=Varsayılan Web İstemcileri
+defaulthooks.desc=Web İstemcileri, belirli Gitea olayları tetiklendiğinde otomatik olarak HTTP POST isteklerini sunucuya yapar. Burada tanımlanan Web İstemcileri varsayılandır ve tüm yeni depolara kopyalanır. <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">web istemcileri kılavuzunda</a> daha fazla bilgi edinin.
 defaulthooks.add_webhook=Varsayılan Web İstemcisi Ekle
 defaulthooks.update_webhook=Varsayılan Web İstemcisini Güncelle
 
 systemhooks=Sistem Web İstemcileri
+systemhooks.desc=Belirli Gitea olayları tetiklendiğinde Web istemcileri otomatik olarak bir sunucuya HTTP POST istekleri yapar. Burada tanımlanan web istemcileri sistemdeki tüm depolar üzerinde çalışır, bu yüzden lütfen bunun olabilecek tüm performans sonuçlarını göz önünde bulundurun. <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">web istemcileri kılavuzunda</a> daha fazla bilgi edinin.
 systemhooks.add_webhook=Sistem Web İstemcisi Ekle
 systemhooks.update_webhook=Sistem Web İstemcisi Güncelle
 
@@ -2942,11 +2975,11 @@ auths.tip.nextcloud=Aşağıdaki "Ayarlar -> Güvenlik -> OAuth 2.0 istemcisi" m
 auths.tip.dropbox=https://www.dropbox.com/developers/apps adresinde yeni bir uygulama oluştur
 auths.tip.facebook=https://developers.facebook.com/apps adresinde yeni bir uygulama kaydedin ve "Facebook Giriş" ürününü ekleyin
 auths.tip.github=https://github.com/settings/applications/new adresinde yeni bir OAuth uygulaması kaydedin
-auths.tip.gitlab=https://gitlab.com/profile/applications adresinde yeni bir uygulama kaydedin
 auths.tip.google_plus=OAuth2 istemci kimlik bilgilerini https://console.developers.google.com/ adresindeki Google API konsolundan edinin
 auths.tip.openid_connect=Bitiş noktalarını belirlemek için OpenID Connect Discovery URL'sini kullanın (<server>/.well-known/openid-configuration)
 auths.tip.twitter=https://dev.twitter.com/apps adresine gidin, bir uygulama oluşturun ve “Bu uygulamanın Twitter ile oturum açmak için kullanılmasına izin ver” seçeneğinin etkin olduğundan emin olun
 auths.tip.discord=https://discordapp.com/developers/applications/me adresinde yeni bir uygulama kaydedin
+auths.tip.gitea=Yeni bir OAuth2 uygulaması kaydedin. Rehber https://docs.gitea.com/development/oauth2-provider adresinde bulunabilir
 auths.tip.yandex=`https://oauth.yandex.com/client/new adresinde yeni bir uygulama oluşturun. "Yandex.Passport API'sı" bölümünden aşağıdaki izinleri seçin: "E-posta adresine erişim", "Kullanıcı avatarına erişim" ve "Kullanıcı adına, ad ve soyadına, cinsiyete erişim"`
 auths.tip.mastodon=Kimlik doğrulaması yapmak istediğiniz mastodon örneği için özel bir örnek URL girin (veya varsayılan olanı kullanın)
 auths.edit=Kimlik Doğrulama Kaynağı Düzenle
@@ -3126,8 +3159,10 @@ monitor.queue.name=İsim
 monitor.queue.type=Tür
 monitor.queue.exemplar=Örnek Türü
 monitor.queue.numberworkers=Çalışan Sayısı
+monitor.queue.activeworkers=Etkin Çalışanlar
 monitor.queue.maxnumberworkers=En Fazla Çalışan Sayısı
 monitor.queue.numberinqueue=Kuyruktaki Sayı
+monitor.queue.review_add=Çalışanları İncele / Ekle
 monitor.queue.settings.title=Havuz Ayarları
 monitor.queue.settings.desc=Havuzlar, çalışan kuyruğu tıkanmasına bir yanıt olarak dinamik olarak büyürler.
 monitor.queue.settings.maxnumberworkers=En fazla çalışan Sayısı
@@ -3153,6 +3188,7 @@ notices.desc=Açıklama
 notices.op=İşlem
 notices.delete_success=Sistem bildirimleri silindi.
 
+
 [action]
 create_repo=depo <a href="%s">%s</a> oluşturuldu
 rename_repo=<code>%[1]s</code> olan depo adını <a href="%[2]s">%[3]s</a> buna çevirdi
@@ -3337,6 +3373,8 @@ rpm.registry=Bu kütüğü komut satırını kullanarak kurun:
 rpm.distros.redhat=RedHat tabanlı dağıtımlarda
 rpm.distros.suse=SUSE tabanlı dağıtımlarda
 rpm.install=Paketi kurmak için, aşağıdaki komutu çalıştırın:
+rpm.repository=Depo Bilgisi
+rpm.repository.architectures=Mimariler
 rubygems.install=Paketi gem ile kurmak için, şu komutu çalıştırın:
 rubygems.install2=veya paketi Gemfile dosyasına ekleyin:
 rubygems.dependencies.runtime=Çalışma Zamanı Bağımlılıkları
@@ -3454,23 +3492,29 @@ runners.status.idle=Boşta
 runners.status.active=Etkin
 runners.status.offline=Çevrimdışı
 runners.version=Sürüm
+runners.reset_registration_token=Kayıt tokenini sıfırla
 runners.reset_registration_token_success=Çalıştırıcı kayıt belirteci başarıyla sıfırlandı
 
 runs.all_workflows=Tüm İş Akışları
 runs.commit=İşle
+runs.scheduled=Zamanlanmış
 runs.pushed_by=iten
 runs.invalid_workflow_helper=İş akışı yapılandırma dosyası geçersiz. Lütfen yapılandırma dosyanızı denetleyin: %s
+runs.no_matching_online_runner_helper=Şu etiket ile eşleşen çevrimiçi çalıştırıcı bulunamadı: %s
 runs.actor=Aktör
 runs.status=Durum
 runs.actors_no_select=Tüm aktörler
 runs.status_no_select=Tüm durumlar
 runs.no_results=Eşleşen sonuç yok.
+runs.no_workflows=Henüz hiç bir iş akışı yok.
 runs.no_runs=İş akışı henüz hiç çalıştırılmadı.
+runs.empty_commit_message=(boş işleme iletisi)
 
 workflow.disable=İş Akışını Devre Dışı Bırak
 workflow.disable_success='%s' iş akışı başarıyla devre dışı bırakıldı.
 workflow.enable=İş Akışını Etkinleştir
 workflow.enable_success='%s' iş akışı başarıyla etkinleştirildi.
+workflow.disabled=İş akışı devre dışı.
 
 need_approval_desc=Değişiklik isteği çatalında iş akışı çalıştırmak için onay gerekiyor.
 
@@ -3481,7 +3525,6 @@ variables.none=Henüz hiçbir değişken yok.
 variables.deletion=Değişkeni kaldır
 variables.deletion.description=Bir değişkeni kaldırma kalıcıdır ve geri alınamaz. Devam edilsin mi?
 variables.description=Değişkenler belirli işlemlere aktarılacaktır, bunun dışında okunamaz.
-variables.id_not_exist=%d kimlikli değişken mevcut değil.
 variables.edit=Değişkeni Düzenle
 variables.deletion.failed=Değişken kaldırılamadı.
 variables.deletion.success=Değişken kaldırıldı.
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index 4cd6c44571..e8a3acedda 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -101,6 +101,15 @@ concept_user_organization=Організація
 
 name=Назва
 
+filter=Фільтр
+filter.is_archived=Архівовані
+filter.is_template=Шаблон
+filter.public=Публічний
+filter.private=Приватний
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -236,7 +245,6 @@ collaborative_repos=Спільні репозиторії
 my_orgs=Мої організації
 my_mirrors=Мої дзеркала
 view_home=Переглянути %s
-search_repos=Шукати репозиторій…
 filter=Інші фільтри
 filter_by_team_repositories=Фільтрувати за репозиторіями команд
 feed_of=`Стрічка "%s"`
@@ -257,14 +265,7 @@ issues.in_your_repos=В ваших репозиторіях
 repos=Репозиторії
 users=Користувачі
 organizations=Організації
-search=Пошук
 code=Код
-search.fuzzy=Неточний
-search.match=Відповідність
-repo_no_results=Відповідних репозиторіїв не знайдено.
-user_no_results=Відповідних користувачів не знайдено.
-org_no_results=Відповідних організацій не знайдено.
-code_no_results=Відповідний пошуковому запитанню код не знайдено.
 code_last_indexed_at=Останні індексовані %s
 
 [auth]
@@ -277,7 +278,6 @@ remember_me=Запам’ятати цей пристрій
 forgot_password_title=Забув пароль
 forgot_password=Забули пароль?
 sign_up_now=Потрібен обліковий запис? Зареєструйтеся зараз.
-confirmation_mail_sent_prompt=Новий лист для підтвердження було відправлено на <b>%s</b>, будь ласка, перевірте вашу поштову скриньку протягом %s для завершення реєстрації.
 must_change_password=Оновіть свій пароль
 allow_password_change=Вимагати в користувача змінити пароль (рекомендується)
 reset_password_mail_sent_prompt=Електронний лист із підтвердженням надіслано <b>%s</b>. Перевірте папку 'Вхідні' в межах наступних %s, щоб завершити процес відновлення облікового запису.
@@ -466,6 +466,7 @@ auth_failed=Помилка автентифікації: %v
 
 target_branch_not_exist=Цільової гілки не існує.
 
+
 [user]
 change_avatar=Змінити свій аватар…
 repositories=Репозиторії
@@ -482,6 +483,7 @@ user_bio=Біографія
 disabled_public_activity=Цей користувач вимкнув публічний показ діяльності.
 
 
+
 [settings]
 profile=Профіль
 account=Обліковий запис
@@ -598,7 +600,6 @@ gpg_invalid_token_signature=Наданий ключ GPG, підпис і ток
 gpg_token_required=Вам потрібно надати підпис для нижчевказаного токена
 gpg_token=Токен
 gpg_token_help=Ви можете створити підпис за допомогою:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Текстовий (armored) підпис GPG
 key_signature_gpg_placeholder=`Починається з "-----BEGIN PGP SIGNATURE-----"`
 ssh_key_verified=Перевірений ключ
@@ -737,7 +738,6 @@ fork_repo=Форкнути репозиторій
 fork_from=Форк з
 fork_visibility_helper=Неможливо змінити видимість форкнутого репозиторію.
 use_template=Застосувати цей шаблон
-clone_in_vsc=Клонувати у VS Code
 download_zip=Завантажити ZIP
 download_tar=Завантажити TAR.GZ
 download_bundle=Завантажити BUNDLE
@@ -979,8 +979,6 @@ editor.require_signed_commit=Гілка вимагає підписаного к
 commits.desc=Переглянути історію зміни коду.
 commits.commits=Коміти
 commits.nothing_to_compare=Ці гілки однакові.
-commits.search=Знайти коміт…
-commits.find=Пошук
 commits.search_all=Усі гілки
 commits.author=Автор
 commits.message=Повідомлення
@@ -1018,7 +1016,6 @@ projects.type.basic_kanban=Спрощений канбан
 projects.type.bug_triage=Сортування помилок
 projects.template.desc=Шаблон проєкту
 projects.template.desc_helper=Оберіть шаблон проєкту, аби почати
-projects.type.uncategorized=Без категорії
 projects.column.edit_title=Назва
 projects.column.new_title=Назва
 projects.column.color=Колір
@@ -1310,7 +1307,6 @@ pulls.compare_compare=pull з
 pulls.switch_comparison_type=Перемкнути вигляд порівняння
 pulls.switch_head_and_base=Поміняти місцями основну та базову гілку
 pulls.filter_branch=Фільтр по гілці
-pulls.no_results=Результатів не знайдено.
 pulls.nothing_to_compare=Ці гілки однакові. Немає необхідності створювати запитів на злиття.
 pulls.nothing_to_compare_and_allow_empty_pr=Одинакові гілки. Цей PR буде порожнім.
 pulls.has_pull_request=`Запит злиття для цих гілок вже існує: <a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1508,12 +1504,7 @@ activity.git_stats_and_deletions=та
 activity.git_stats_deletion_1=%d видалений
 activity.git_stats_deletion_n=%d видалені
 
-search=Пошук
-search.search_repo=Пошук репозиторію
-search.fuzzy=Неточний
-search.match=Збігається
-search.results=Результати пошуку для "%s" в <a href="%s">%s</a>
-search.code_no_results=Відповідний пошуковому запитанню код не знайдено.
+contributors.contribution_type.commits=Коміти
 
 settings=Налаштування
 settings.desc=У налаштуваннях ви можете змінювати різні параметри цього репозиторія
@@ -1630,7 +1621,6 @@ settings.delete_collaborator=Видалити
 settings.collaborator_deletion=Видалити співавтора
 settings.collaborator_deletion_desc=Цей користувач більше не матиме доступу для спільної роботи в цьому репозиторії після видалення. Ви хочете продовжити?
 settings.remove_collaborator_success=Співавтор видалений.
-settings.search_user_placeholder=Пошук користувача…
 settings.org_not_allowed_to_be_collaborator=Організації не можуть бути додані як співавтори.
 settings.change_team_access_not_allowed=Зміна доступу команди до репозитарію обмежена власником організації
 settings.team_not_in_organization=Команда та репозитарій мають привязки до різних організацій
@@ -1638,7 +1628,6 @@ settings.teams=Команди
 settings.add_team=Додати Команду
 settings.add_team_duplicate=Команда вже має привязку до репозитарію
 settings.add_team_success=Команда отримала доступ до репозиторію.
-settings.search_team=Знайти команду…
 settings.change_team_permission_tip=Дозволи команди встановлюються на сторінці налаштувань команди та не можуть бути заданими для кожного з репозиторіїв окремо
 settings.delete_team_tip=Ця команда має доступ до всіх репозиторіїв та не може бути видалена
 settings.remove_team_success=Доступ команди до репозиторію видалений.
@@ -1755,9 +1744,7 @@ settings.protect_whitelist_committers=Білий список обмеження
 settings.protect_whitelist_committers_desc=Лише користувачі та команди з білого списку зможуть виконувати push в цій гілці (за виключеням force push).
 settings.protect_whitelist_deploy_keys=Білий список ключів розгортання з правом на запис.
 settings.protect_whitelist_users=Користувачі, які можуть робити push в цю гілку:
-settings.protect_whitelist_search_users=Пошук користувачів…
 settings.protect_whitelist_teams=Команди, учасники яких можуть робити push в цю гілку:
-settings.protect_whitelist_search_teams=Пошук команд…
 settings.protect_merge_whitelist_committers=Обмежити право на прийняття Pull Request'ів в цю гілку списком
 settings.protect_merge_whitelist_committers_desc=Ви можете додавати користувачів або цілі команди в 'білий' список цієї гілки. Тільки присутні в списку зможуть приймати запити на злиття. В іншому випадку будь-хто з правами запису до головного репозиторію буде володіти такою можливістю.
 settings.protect_merge_whitelist_users=Користувачі з правом на прийняття Pull Request'ів в цю гілку:
@@ -1961,6 +1948,8 @@ error.csv.too_large=Не вдається відобразити цей файл
 error.csv.unexpected=Не вдається відобразити цей файл, тому що він містить неочікуваний символ в рядку %d і стовпці %d.
 error.csv.invalid_field_count=Не вдається відобразити цей файл, тому що він має неправильну кількість полів у рядку %d.
 
+[graphs]
+
 [org]
 org_name_holder=Назва організації
 org_full_name_holder=Повна назва організації
@@ -2052,7 +2041,6 @@ teams.write_permission_desc=Ця команда надає доступ на <st
 teams.admin_permission_desc=Ця команда надає <strong>адміністраторський</strong> доступ: учасники можуть читати, виконувати push команди та додавати співробітників до репозиторію.
 teams.create_repo_permission_desc=Крім того, ця команда надає дозвіл <strong>Створити репозиторій</strong>: учасники можуть створювати нові репозиторії в організації.
 teams.repositories=Репозиторії команди
-teams.search_repo_placeholder=Пошук репозиторію…
 teams.remove_all_repos_title=Видалити всі репозиторії команди
 teams.remove_all_repos_desc=Це видалить усі репозиторії команди.
 teams.add_all_repos_title=Додати всі репозиторії
@@ -2077,6 +2065,8 @@ hooks=Веб-хуки
 authentication=Джерела автентифікації
 emails=Електронні адреси Користувача
 config=Конфігурація
+config_summary=Підсумок
+config_settings=Налаштування
 notices=Сповіщення системи
 monitor=Моніторинг
 first_page=Перша
@@ -2084,7 +2074,6 @@ last_page=Остання
 total=Разом: %d
 
 dashboard.statistic=Підсумок
-dashboard.operations=Технічне обслуговування
 dashboard.system_status=Статус системи
 dashboard.operation_name=Назва операції
 dashboard.operation_switch=Перемкнути
@@ -2225,9 +2214,6 @@ repos.unadopted.no_more=Не знайдено більше неприйняти
 repos.owner=Власник
 repos.name=Назва
 repos.private=Приватний
-repos.watches=Стежать
-repos.stars=В обраному
-repos.forks=Форки
 repos.issues=Задачі
 repos.size=Розмір
 
@@ -2325,7 +2311,6 @@ auths.tip.nextcloud=`Зареєструйте нового споживача OA
 auths.tip.dropbox=Додайте новий додаток на https://www.dropbox.com/developers/apps
 auths.tip.facebook=`Створіть новий додаток на https://developers.facebook.com/apps і додайте модуль "Facebook Login"`
 auths.tip.github=Додайте OAuth додаток на https://github.com/settings/applications/new
-auths.tip.gitlab=Додайте новий додаток на https://gitlab.com/profile/applications
 auths.tip.google_plus=Отримайте облікові дані клієнта OAuth2 в консолі Google API на сторінці https://console.developers.google.com/
 auths.tip.openid_connect=Використовуйте OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) для автоматичної настройки входу OAuth
 auths.tip.twitter=Перейдіть на https://dev.twitter.com/apps, створіть програму і переконайтеся, що включена опція «Дозволити цю програму для входу в систему за допомогою Twitter»
@@ -2510,6 +2495,7 @@ notices.desc=Опис
 notices.op=Оп.
 notices.delete_success=Сповіщення системи були видалені.
 
+
 [action]
 create_repo=створив(ла) репозиторій <a href="%s">%s</a>
 rename_repo=репозиторій перейменовано з <code>%[1]s</code> на <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 6d22468c9d..3e907eabfd 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -25,6 +25,7 @@ enable_javascript=此网站需要 JavaScript。
 toc=目录
 licenses=许可证
 return_to_gitea=返回 Gitea
+more_items=更多选项
 
 username=用户名
 email=电子邮件地址
@@ -113,6 +114,7 @@ loading=正在加载...
 error=错误
 error404=您正尝试访问的页面 <strong>不存在</strong> 或 <strong>您尚未被授权</strong> 查看该页面。
 go_back=返回
+invalid_data=无效数据: %v
 
 never=从不
 unknown=未知
@@ -123,6 +125,7 @@ pin=固定
 unpin=取消置顶
 
 artifacts=制品
+confirm_delete_artifact=您确定要删除制品'%s'吗?
 
 archived=已归档
 
@@ -141,6 +144,38 @@ confirm_delete_selected=确认删除所有选中项目?
 name=名称
 value=值
 
+filter=过滤
+filter.clear=清除筛选器
+filter.is_archived=已归档
+filter.not_archived=非存档
+filter.is_fork=派生
+filter.not_fork=非派生
+filter.is_mirror=镜像
+filter.not_mirror=非镜像
+filter.is_template=模板
+filter.not_template=非模板
+filter.public=公开
+filter.private=私有库
+
+no_results_found=未找到结果
+
+[search]
+search=搜索...
+type_tooltip=搜索类型
+fuzzy=模糊
+match=匹配
+repo_kind=搜索仓库...
+user_kind=搜索用户...
+org_kind=搜索组织...
+team_kind=搜索团队...
+code_kind=搜索代码...
+package_kind=搜索软件包...
+project_kind=搜索项目...
+branch_kind=搜索分支...
+commit_kind=搜索提交记录...
+runner_kind=搜索runners...
+no_results=未找到匹配结果
+
 [aria]
 navbar=导航栏
 footer=页脚
@@ -305,6 +340,7 @@ env_config_keys=环境配置
 env_config_keys_prompt=以下环境变量也将应用于您的配置文件:
 
 [home]
+nav_menu=导航菜单
 uname_holder=用户名或邮箱
 password_holder=密码
 switch_dashboard_context=切换控制面板用户
@@ -314,7 +350,6 @@ collaborative_repos=参与协作的仓库
 my_orgs=我的组织
 my_mirrors=我的镜像
 view_home=访问 %s
-search_repos=查找仓库…
 filter=其他过滤器
 filter_by_team_repositories=按团队仓库筛选
 feed_of=`"%s"的源`
@@ -335,20 +370,8 @@ issues.in_your_repos=在您的仓库中
 repos=仓库
 users=用户
 organizations=组织
-search=搜索
 go_to=转到
 code=代码
-search.type.tooltip=搜索类型
-search.fuzzy=模糊
-search.fuzzy.tooltip=包含近似匹配搜索词的结果
-search.match=匹配
-search.match.tooltip=仅包含精确匹配搜索词的结果
-code_search_unavailable=目前代码搜索不可用。请与网站管理员联系。
-repo_no_results=未找到匹配的仓库。
-user_no_results=未找到匹配的用户。
-org_no_results=未找到匹配的组织。
-code_no_results=未找到与搜索字词匹配的源代码。
-code_search_results=“%s” 的搜索结果是
 code_last_indexed_at=最后索引于 %s
 relevant_repositories_tooltip=派生的仓库,以及缺少主题、图标和描述的仓库将被隐藏。
 relevant_repositories=只显示相关的仓库, <a href="%s">显示未过滤结果</a>。
@@ -366,7 +389,6 @@ forgot_password_title=忘记密码
 forgot_password=忘记密码?
 sign_up_now=还没帐户?马上注册。
 sign_up_successful=帐户创建成功。欢迎!
-confirmation_mail_sent_prompt=一封新的确认邮件已经被发送至 <b>%s</b>,请检查您的收件箱并在 %s 内完成确认注册操作。
 must_change_password=更新您的密码
 allow_password_change=要求用户更改密码(推荐)
 reset_password_mail_sent_prompt=确认电子邮件已被发送到 <b>%s</b>。请您在 %s 内检查您的收件箱 ,完成密码重置过程。
@@ -423,6 +445,7 @@ authorization_failed_desc=因为检测到无效请求,授权失败。请尝试
 sspi_auth_failed=SSPI 认证失败
 password_pwned=此密码出现在 <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">被盗密码</a> 列表上并且曾经被公开。 请使用另一个密码再试一次。
 password_pwned_err=无法完成对 HaveIBeenPwned 的请求
+last_admin=您不能删除最后一个管理员。必须至少保留一个管理员。
 
 [mail]
 view_it_on=在 %s 上查看
@@ -588,6 +611,8 @@ org_still_own_packages=该组织仍然是一个或多个软件包的拥有者,
 
 target_branch_not_exist=目标分支不存在。
 
+admin_cannot_delete_self=当您是管理员时,您不能删除自己。请先移除您的管理员权限
+
 [user]
 change_avatar=修改头像
 joined_on=加入于 %s
@@ -613,6 +638,14 @@ form.name_reserved=用户名 "%s" 被保留。
 form.name_pattern_not_allowed=用户名中不允许使用 "%s" 格式。
 form.name_chars_not_allowed=用户名 "%s" 包含无效字符。
 
+block.block=屏蔽
+block.block.user=屏蔽用户
+block.block.org=屏蔽用户访问组织
+block.block.failure=屏蔽用户失败: %s
+block.unblock=取消屏蔽
+block.title=屏蔽一个用户
+block.info_2=关注你的账号
+
 [settings]
 profile=个人信息
 account=账号
@@ -757,7 +790,6 @@ gpg_invalid_token_signature=提供的 GPG 密钥、签名和令牌不匹配或
 gpg_token_required=您必须为下面的令牌提供签名
 gpg_token=令牌
 gpg_token_help=您可以使用以下方式生成签名:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=GPG 增强签名
 key_signature_gpg_placeholder=以 '-----BEGIN PGP PUBLIC KEY BLOCK-----' 开头
 verify_gpg_key_success=GPG 密钥 %s 已被验证。
@@ -951,7 +983,6 @@ fork_branch=要克隆到 Fork 的分支
 all_branches=所有分支
 fork_no_valid_owners=这个代码仓库无法被派生,因为没有有效的所有者。
 use_template=使用此模板
-clone_in_vsc=在 VS Code 中克隆
 download_zip=下载 ZIP
 download_tar=下载 TAR.GZ
 download_bundle=下载 BUNDLE
@@ -967,6 +998,8 @@ issue_labels_helper=选择一个工单标签集
 license=授权许可
 license_helper=选择授权许可文件。
 license_helper_desc=许可证说明了其他人可以和不可以用您的代码做什么。不确定哪一个适合你的项目?见 <a target="_blank" rel="noopener noreferrer" href="%s">选择一个许可证</a>
+object_format=对象格式
+object_format_helper=仓库的对象格式。之后无法更改。SHA1 是最兼容的。
 readme=自述
 readme_helper=选择自述文件模板。
 readme_helper_desc=这是您可以为您的项目撰写完整描述的地方。
@@ -984,6 +1017,7 @@ mirror_prune=修剪
 mirror_prune_desc=删除过时的远程跟踪引用
 mirror_interval=镜像间隔 (有效的时间单位是 'h', 'm', 's')。0 禁用自动定期同步 (最短间隔: %s)
 mirror_interval_invalid=镜像间隔无效。
+mirror_sync=已同步
 mirror_sync_on_commit=推送提交时同步
 mirror_address=从 URL 克隆
 mirror_address_desc=在授权框中输入必要的凭据。
@@ -1034,6 +1068,7 @@ desc.public=公开
 desc.template=模板
 desc.internal=内部
 desc.archived=已存档
+desc.sha256=SHA256
 
 template.items=模板选项
 template.git_content=Git数据(默认分支)
@@ -1184,6 +1219,8 @@ audio_not_supported_in_browser=您的浏览器不支持使用 HTML5 'video' 标
 stored_lfs=存储到Git LFS
 symbolic_link=符号链接
 executable_file=可执行文件
+vendored=被供应的
+generated=已生成的
 commit_graph=提交图
 commit_graph.select=选择分支
 commit_graph.hide_pr_refs=隐藏合并请求
@@ -1270,9 +1307,8 @@ commits.desc=浏览代码修改历史
 commits.commits=次代码提交
 commits.no_commits=没有共同的提交。%s 和 %s 的历史完全不同。
 commits.nothing_to_compare=这些分支是相同的。
-commits.search=搜索提交历史
 commits.search.tooltip=`您可以在关键词前加上前缀,如"author:", "committer:", "after:", 或"before:", 例如 "retrin author:Alice before:2019-01-13"`
-commits.find=搜索
+commits.search_branch=此分支
 commits.search_all=所有分支
 commits.author=作者
 commits.message=备注
@@ -1323,7 +1359,6 @@ projects.type.basic_kanban=基础看板
 projects.type.bug_triage=Bug分类看板
 projects.template.desc=项目模板
 projects.template.desc_helper=选择一个项目模板以开始
-projects.type.uncategorized=未分类
 projects.column.edit=编辑列
 projects.column.edit_title=名称
 projects.column.new_title=名称
@@ -1331,8 +1366,6 @@ projects.column.new_submit=创建列
 projects.column.new=创建列
 projects.column.set_default=设为默认
 projects.column.set_default_desc=设置此列为未分类问题和合并请求的默认值
-projects.column.unset_default=取消设为默认
-projects.column.unset_default_desc=取消此列为默认值
 projects.column.delete=删除列
 projects.column.deletion_desc=删除项目列会将所有相关问题移到“未分类”。是否继续?
 projects.column.color=彩色
@@ -1446,7 +1479,6 @@ issues.filter_sort.moststars=点赞由多到少
 issues.filter_sort.feweststars=点赞由少到多
 issues.filter_sort.mostforks=派生由多到少
 issues.filter_sort.fewestforks=派生由少到多
-issues.keyword_search_unavailable=关键词搜索目前不可用。请联系网站管理员。
 issues.action_open=开启
 issues.action_close=关闭
 issues.action_label=标签
@@ -1698,7 +1730,6 @@ pulls.compare_compare=拉取从
 pulls.switch_comparison_type=切换比较类型
 pulls.switch_head_and_base=切换 head 和 base
 pulls.filter_branch=过滤分支
-pulls.no_results=未找到结果
 pulls.show_all_commits=显示所有提交
 pulls.show_changes_since_your_last_review=显示自您上次审核以来的更改
 pulls.showing_only_single_commit=仅显示提交 %[1]s 的更改
@@ -1707,6 +1738,7 @@ pulls.select_commit_hold_shift_for_range=选择提交。按住 Shift + 单击选
 pulls.review_only_possible_for_full_diff=只有在查看全部差异时才能进行审核
 pulls.filter_changes_by_commit=按提交筛选
 pulls.nothing_to_compare=分支内容相同,无需创建合并请求。
+pulls.nothing_to_compare_have_tag=所选分支/标签相同。
 pulls.nothing_to_compare_and_allow_empty_pr=这些分支是相等的,此合并请求将为空。
 pulls.has_pull_request=这些分支之间的合并请求已存在: <a href="%[1]s">%[2]s#%[3]d</a>
 pulls.create=创建合并请求
@@ -1901,6 +1933,9 @@ wiki.page_name_desc=输入此 Wiki 页面的名称。特殊名称有:'Home', '
 wiki.original_git_entry_tooltip=查看原始的 Git 文件而不是使用友好链接。
 
 activity=动态
+activity.navbar.code_frequency=代码频率
+activity.navbar.contributors=贡献者
+activity.navbar.recent_commits=最近的提交
 activity.period.filter_label=周期:
 activity.period.daily=1 天
 activity.period.halfweekly=3 天
@@ -1966,16 +2001,10 @@ activity.git_stats_and_deletions=和
 activity.git_stats_deletion_1=删除 %d 行
 activity.git_stats_deletion_n=删除 %d 行
 
-search=搜索
-search.search_repo=搜索仓库...
-search.type.tooltip=搜索类型
-search.fuzzy=模糊
-search.fuzzy.tooltip=包含近似匹配搜索词的结果
-search.match=匹配
-search.match.tooltip=仅包含精确匹配搜索词的结果
-search.results=在 <a href="%[2]s"> %[3]s </a> 中搜索 "%[1]s" 的结果
-search.code_no_results=未找到与搜索字词匹配的源代码。
-search.code_search_unavailable=当前代码搜索不可用。请与网站管理员联系。
+contributors.contribution_type.filter_label=贡献类型:
+contributors.contribution_type.commits=提交
+contributors.contribution_type.additions=更多
+contributors.contribution_type.deletions=删除
 
 settings=设置
 settings.desc=设置是你可以管理仓库设置的地方
@@ -2003,6 +2032,7 @@ settings.mirror_settings.docs.doc_link_title=如何镜像仓库?
 settings.mirror_settings.docs.doc_link_pull_section=文档中的 “从远程仓库拉取” 部分。
 settings.mirror_settings.docs.pulling_remote_title=从远程仓库拉取代码
 settings.mirror_settings.mirrored_repository=镜像库
+settings.mirror_settings.pushed_repository=推送仓库
 settings.mirror_settings.direction=方向
 settings.mirror_settings.direction.pull=拉取
 settings.mirror_settings.direction.push=推送
@@ -2024,6 +2054,8 @@ settings.branches.add_new_rule=添加新规则
 settings.advanced_settings=高级设置
 settings.wiki_desc=启用仓库百科
 settings.use_internal_wiki=使用内置百科
+settings.default_wiki_branch_name=默认百科分支名称
+settings.failed_to_change_default_wiki_branch=更改百科默认分支失败。
 settings.use_external_wiki=使用外部百科
 settings.external_wiki_url=外部 Wiki 链接
 settings.external_wiki_url_error=外部百科链接无效
@@ -2054,6 +2086,10 @@ settings.pulls.default_allow_edits_from_maintainers=默认开启允许维护者
 settings.releases_desc=启用发布
 settings.packages_desc=启用仓库软件包注册中心
 settings.projects_desc=启用仓库项目
+settings.projects_mode_desc=项目模式 (要显示的项目类型)
+settings.projects_mode_repo=仅仓库项目
+settings.projects_mode_owner=仅限用户或组织项目
+settings.projects_mode_all=所有项目
 settings.actions_desc=启用 Actions
 settings.admin_settings=管理员设置
 settings.admin_enable_health_check=启用仓库健康检查 (git fsck)
@@ -2079,6 +2115,7 @@ settings.convert_fork_succeed=此派生仓库已经转换为普通仓库。
 settings.transfer=转移仓库所有权
 settings.transfer.rejected=代码库转移被拒绝。
 settings.transfer.success=代码库转移成功。
+settings.transfer.blocked_user=无法传输仓库,因为您被新的所有者屏蔽。
 settings.transfer_abort=取消转移
 settings.transfer_abort_invalid=你不能取消不存在的代码库转移。
 settings.transfer_abort_success=成功取消了将代码库转让给 %s。
@@ -2101,7 +2138,7 @@ settings.trust_model.collaborator.long=协作者:信任协作者的签名
 settings.trust_model.collaborator.desc=此仓库中协作者的有效签名将被标记为「可信」(无论它们是否是提交者),签名只符合提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。
 settings.trust_model.committer=提交者
 settings.trust_model.committer.long=提交者: 信任与提交者相符的签名 (此特性类似 GitHub,这会强制采用 Gitea 作为提交者和签名者)
-settings.trust_model.committer.desc=有效签名只有和提交者相匹配才会被标记为“受信任”,否则它们将被标记为“不匹配”。这强制 Gitea 成为签名提交的提交者,而实际提交者被加上 Co-authored-by: 和 Co-committed-by: 的标记。 默认的 Gitea 密钥必须撇撇数据库种的一名用户。
+settings.trust_model.committer.desc=有效签名只有和提交者相匹配才会被标记为“受信任”,否则它们将被标记为“不匹配”。这强制 Gitea 成为签名提交的提交者,而实际提交者被加上 Co-authored-by: 和 Co-committed-by: 的标记。 默认的 Gitea 密钥必须匹配数据库中的一名用户。
 settings.trust_model.collaboratorcommitter=协作者+提交者
 settings.trust_model.collaboratorcommitter.long=协作者+提交者:信任协作者同时是提交者的签名
 settings.trust_model.collaboratorcommitter.desc=此仓库中协作者的有效签名在他同时是提交者时将被标记为「可信」,签名只匹配了提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。这会强制 Gitea 成为签名者和提交者,实际的提交者将被标记于提交消息结尾处的「Co-Authored-By:」和「Co-Committed-By:」。默认的 Gitea 签名密钥必须匹配数据库中的一个用户密钥。
@@ -2124,11 +2161,11 @@ settings.add_collaborator_success=协作者添加成功!
 settings.add_collaborator_inactive_user=无法添加未激活的用户作为合作者。
 settings.add_collaborator_owner=不能将所有者添加为协作者。
 settings.add_collaborator_duplicate=合作者已经被添加到本仓库。
+settings.add_collaborator.blocked_user=此写作者被仓库所有者屏蔽,反之亦然。
 settings.delete_collaborator=删除
 settings.collaborator_deletion=删除协作者
 settings.collaborator_deletion_desc=删除协作者后他将无法再对此仓库的访问。继续?
 settings.remove_collaborator_success=协作者删除成功!
-settings.search_user_placeholder=搜索用户...
 settings.org_not_allowed_to_be_collaborator=组织不允许被添加为仓库协作者!
 settings.change_team_access_not_allowed=更改仓库的团队访问权限仅限于组织所有者
 settings.team_not_in_organization=团队不在与仓库相同的组织中
@@ -2136,7 +2173,6 @@ settings.teams=团队
 settings.add_team=添加团队
 settings.add_team_duplicate=团队已经拥有仓库
 settings.add_team_success=团队现在可以访问仓库。
-settings.search_team=搜索团队...
 settings.change_team_permission_tip=团队权限设置于团队设置页面,不能根据仓库更改
 settings.delete_team_tip=该团队仍有仓库, 无法删除
 settings.remove_team_success=团队访问仓库的权限已被删除。
@@ -2289,9 +2325,7 @@ settings.protect_whitelist_committers=受白名单限制的推送
 settings.protect_whitelist_committers_desc=只有列入白名单的用户或团队才能被允许推送到此分支(但不能强行推送)。
 settings.protect_whitelist_deploy_keys=具有推送权限的部署密钥白名单。
 settings.protect_whitelist_users=推送白名单用户:
-settings.protect_whitelist_search_users=搜索用户...
 settings.protect_whitelist_teams=推送白名单团队:
-settings.protect_whitelist_search_teams=搜索团队...
 settings.protect_merge_whitelist_committers=启用合并白名单
 settings.protect_merge_whitelist_committers_desc=仅允许白名单用户或团队合并合并请求到此分支。
 settings.protect_merge_whitelist_users=合并白名单用户:
@@ -2312,6 +2346,8 @@ settings.protect_approvals_whitelist_users=审查者白名单:
 settings.protect_approvals_whitelist_teams=审查团队白名单:
 settings.dismiss_stale_approvals=取消过时的批准
 settings.dismiss_stale_approvals_desc=当新的提交更改合并请求内容被推送到分支时,旧的批准将被撤销。
+settings.ignore_stale_approvals=忽略过期批准
+settings.ignore_stale_approvals_desc=对旧提交(过期审核)的批准将不计入 PR 的批准数。如果过期审查已被驳回,则与此无关。
 settings.require_signed_commits=需要签名提交
 settings.require_signed_commits_desc=拒绝推送未签名或无法验证的提交到分支
 settings.protect_branch_name_pattern=受保护的分支名称模式
@@ -2367,6 +2403,7 @@ settings.archive.error=仓库在归档时出现异常。请通过日志获取详
 settings.archive.error_ismirror=请不要对镜像仓库归档,谢谢!
 settings.archive.branchsettings_unavailable=已归档仓库无法进行分支设置。
 settings.archive.tagsettings_unavailable=已归档仓库的Git标签设置不可用。
+settings.archive.mirrors_unavailable=如果仓库已被归档,镜像将不可用。
 settings.unarchive.button=撤销仓库归档
 settings.unarchive.header=撤销此仓库归档
 settings.unarchive.text=撤销归档将恢复仓库接收提交、推送,以及新工单和合并请求的能力。
@@ -2533,7 +2570,6 @@ branch.default_deletion_failed=不能删除默认分支"%s"。
 branch.restore=`还原分支 "%s"`
 branch.download=`下载分支 "%s"`
 branch.rename=`重命名分支 "%s"`
-branch.search=搜索分支
 branch.included_desc=此分支是默认分支的一部分
 branch.included=已包含
 branch.create_new_branch=从下列分支创建分支:
@@ -2564,6 +2600,16 @@ find_file.no_matching=没有找到匹配的文件
 error.csv.too_large=无法渲染此文件,因为它太大了。
 error.csv.unexpected=无法渲染此文件,因为它包含了意外字符,其位于第 %d 行和第 %d 列。
 error.csv.invalid_field_count=无法渲染此文件,因为它在第 %d 行中的字段数有误。
+error.broken_git_hook=此仓库的 Git 钩子似乎已损坏。 请按照 <a target="_blank" rel="noreferrer" href="%s">文档</a> 来修复它们,然后推送一些提交来刷新状态。
+
+[graphs]
+component_loading=正在加载 %s...
+component_loading_failed=无法加载 %s
+component_loading_info=这可能需要一点…
+component_failed_to_load=意外的错误发生了。
+code_frequency.what=代码频率
+contributors.what=贡献
+recent_commits.what=最近的提交
 
 [org]
 org_name_holder=组织名称
@@ -2669,7 +2715,6 @@ teams.write_permission_desc=该团队拥有对所属仓库的 <strong>读取</st
 teams.admin_permission_desc=该团队拥有一定的 <strong>管理</strong> 权限,团队成员可以读取、克隆、推送以及添加其它仓库协作者。
 teams.create_repo_permission_desc=此外,该团队拥有了 <strong>创建仓库</strong> 的权限:成员可以在组织中创建新的仓库。
 teams.repositories=团队仓库
-teams.search_repo_placeholder=搜索仓库...
 teams.remove_all_repos_title=移除所有团队仓库
 teams.remove_all_repos_desc=这将从团队中移除所有仓库。
 teams.add_all_repos_title=添加所有仓库
@@ -2678,6 +2723,7 @@ teams.add_nonexistent_repo=您尝试添加的仓库不存在,请先创建它
 teams.add_duplicate_users=用户已经是团队成员。
 teams.repos.none=此团队无法访问任何仓库。
 teams.members.none=团队中没有成员。
+teams.members.blocked_user=不能添加用户因为他已经被该组织屏蔽。
 teams.specific_repositories=指定仓库
 teams.specific_repositories_helper=团队成员将只能访问添加到团队的仓库。 选择此项 <strong>将不会</strong> 自动删除已经添加的仓库。
 teams.all_repositories=所有仓库
@@ -2690,7 +2736,9 @@ teams.invite.by=邀请人 %s
 teams.invite.description=请点击下面的按钮加入团队。
 
 [admin]
+maintenance=维护
 dashboard=管理面板
+self_check=自我检查
 identity_access=身份及认证
 users=帐户管理
 organizations=组织管理
@@ -2701,6 +2749,8 @@ integrations=集成
 authentication=认证源
 emails=用户邮件
 config=应用配置
+config_summary=摘要
+config_settings=组织设置
 notices=系统提示
 monitor=监控面板
 first_page=首页
@@ -2710,7 +2760,7 @@ settings=管理设置
 
 dashboard.new_version_hint=Gitea %s 现已可用,您正在运行 %s。查看 <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">博客</a> 了解详情。
 dashboard.statistic=摘要
-dashboard.operations=维护操作
+dashboard.maintenance_operations=运维
 dashboard.system_status=系统状态
 dashboard.operation_name=操作名称
 dashboard.operation_switch=开关
@@ -2736,6 +2786,7 @@ dashboard.delete_missing_repos=删除所有丢失 Git 文件的仓库
 dashboard.delete_missing_repos.started=删除所有丢失 Git 文件的仓库任务已启动。
 dashboard.delete_generated_repository_avatars=删除生成的仓库头像
 dashboard.sync_repo_branches=将缺少的分支从 git 数据同步到数据库
+dashboard.sync_repo_tags=从 git 数据同步标签到数据库
 dashboard.update_mirrors=更新镜像仓库
 dashboard.repo_health_check=健康检查所有仓库
 dashboard.check_repo_stats=检查所有仓库统计
@@ -2790,6 +2841,7 @@ dashboard.stop_endless_tasks=停止永不停止的任务
 dashboard.cancel_abandoned_jobs=取消丢弃的任务
 dashboard.start_schedule_tasks=开始调度任务
 dashboard.sync_branch.started=分支同步已开始
+dashboard.sync_tag.started=标签同步已开始
 dashboard.rebuild_issue_indexer=重建工单索引
 
 users.user_manage_panel=用户帐户管理
@@ -2875,9 +2927,6 @@ repos.unadopted.no_more=找不到更多未被收录的仓库
 repos.owner=所有者
 repos.name=名称
 repos.private=私有库
-repos.watches=关注数
-repos.stars=点赞数
-repos.forks=派生数
 repos.issues=工单数
 repos.size=大小
 repos.lfs_size=LFS 大小
@@ -3002,7 +3051,7 @@ auths.tip.nextcloud=使用下面的菜单“设置(Settings) -> 安全(Sec
 auths.tip.dropbox=在 https://www.dropbox.com/developers/apps 上创建一个新的应用程序
 auths.tip.facebook=`在 https://developers.facebook.com/apps 注册一个新的应用,并添加产品"Facebook 登录"`
 auths.tip.github=在 https://github.com/settings/applications/new 注册一个 OAuth 应用程序
-auths.tip.gitlab=在 https://gitlab.com/profile/applications 上注册新应用程序
+auths.tip.gitlab_new=在 https://gitlab.com/-/profile/applications 注册一个新的应用
 auths.tip.google_plus=从谷歌 API 控制台 (https://console.developers.google.com/) 获得 OAuth2 客户端凭据
 auths.tip.openid_connect=使用 OpenID 连接发现 URL (<server>/.well-known/openid-configuration) 来指定终点
 auths.tip.twitter=访问 https://dev.twitter.com/apps,创建应用并确保启用了"允许此应用程序用于登录 Twitter"的选项。
@@ -3138,6 +3187,7 @@ config.picture_config=图片和头像配置
 config.picture_service=图片服务
 config.disable_gravatar=禁用 Gravatar 头像
 config.enable_federated_avatar=启用 Federated Avatars
+config.open_with_editor_app_help=用于克隆菜单的编辑器。如果为空将使用默认值。展开可以查看默认值。
 
 config.git_config=Git 配置
 config.git_disable_diff_highlight=禁用差异对比语法高亮
@@ -3216,6 +3266,14 @@ notices.desc=提示描述
 notices.op=操作
 notices.delete_success=系统通知已被删除。
 
+self_check.no_problem_found=尚未发现问题。
+self_check.startup_warnings=启动警告:
+self_check.database_collation_mismatch=期望数据库使用的校验方式:%s
+self_check.database_collation_case_insensitive=数据库正在使用一个校验 %s, 这是一个不敏感的校验. 虽然Gitea可以与它合作,但可能有一些罕见的情况不如预期的那样起作用。
+self_check.database_inconsistent_collation_columns=数据库正在使用%s的排序规则,但是这些列使用了不匹配的排序规则。这可能会造成一些意外问题。
+self_check.database_fix_mysql=对于MySQL/MariaDB用户,您可以使用“gitea doctor convert”命令来解决校验问题。 或者您也可以通过 "ALTER ... COLLATE ..." 这样的SQL 来手动解决这个问题。
+self_check.database_fix_mssql=对于MSSQL用户,您现在只能通过"ALTER ... COLLATE ..."SQLs手动解决这个问题。
+
 [action]
 create_repo=创建了仓库 <a href="%s">%s</a>
 rename_repo=重命名仓库 <code>%[1]s</code> 为 <a href="%[2]s">%[3]s</a>
@@ -3400,6 +3458,9 @@ rpm.registry=从命令行设置此注册中心:
 rpm.distros.redhat=在基于 RedHat 的发行版
 rpm.distros.suse=在基于 SUSE 的发行版
 rpm.install=要安装包,请运行以下命令:
+rpm.repository=仓库信息
+rpm.repository.architectures=架构
+rpm.repository.multiple_groups=此软件包可在多个组中使用。
 rubygems.install=要使用 gem 安装软件包,请运行以下命令:
 rubygems.install2=或将它添加到 Gemfile:
 rubygems.dependencies.runtime=运行时依赖
@@ -3526,14 +3587,15 @@ runs.scheduled=已计划的
 runs.pushed_by=推送者
 runs.invalid_workflow_helper=工作流配置文件无效。请检查您的配置文件: %s
 runs.no_matching_online_runner_helper=没有匹配标签的在线 runner: %s
+runs.no_job_without_needs=工作流必须包含至少一个没有依赖关系的作业。
 runs.actor=操作者
 runs.status=状态
 runs.actors_no_select=所有操作者
 runs.status_no_select=所有状态
 runs.no_results=没有匹配的结果。
 runs.no_workflows=目前还没有工作流。
-runs.no_workflows.quick_start=不知道如何启动Gitea Action?请参阅 <a target="_blank" rel="noopener noreferrer" href="%s">快速启动指南</a>
-runs.no_workflows.documentation=更多有关 Gitea Action 的信息,请访问 <a target="_blank" rel="noopener noreferrer" href="%s">文档</a>。
+runs.no_workflows.quick_start=不知道如何使用 Gitea Actions吗?请查看 <a target="_blank" rel="noopener noreferrer" href="%s">快速启动指南</a>。
+runs.no_workflows.documentation=关于Gitea Actions的更多信息,请参阅 <a target="_blank" rel="noopener noreferrer" href="%s">文档</a>。
 runs.no_runs=工作流尚未运行过。
 runs.empty_commit_message=(空白的提交消息)
 
@@ -3552,7 +3614,7 @@ variables.none=目前还没有变量。
 variables.deletion=删除变量
 variables.deletion.description=删除变量是永久性的,无法撤消。继续吗?
 variables.description=变量将被传给特定的 Actions,其它情况将不能读取
-variables.id_not_exist=ID %d 变量不存在。
+variables.id_not_exist=ID为 %d 的变量不存在。
 variables.edit=编辑变量
 variables.deletion.failed=删除变量失败。
 variables.deletion.success=变量已被删除。
diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini
index d4074026fd..d4b65239a6 100644
--- a/options/locale/locale_zh-HK.ini
+++ b/options/locale/locale_zh-HK.ini
@@ -61,6 +61,12 @@ concept_code_repository=儲存庫
 
 name=組織名稱
 
+filter.is_template=樣板
+filter.private=私有庫
+
+
+[search]
+
 [aria]
 
 [heatmap]
@@ -116,13 +122,11 @@ issues.in_your_repos=屬於該用戶儲存庫的
 repos=儲存庫
 users=使用者
 organizations=組織
-search=搜尋
 
 [auth]
 register_helper_msg=已經註冊?立即登錄!
 forgot_password_title=忘記密碼
 forgot_password=忘記密碼?
-confirmation_mail_sent_prompt=一封新的確認郵件已發送至 <b>%s</b>。請檢查您的收件箱並在 %s 小時內完成確認註冊操作。
 active_your_account=啟用您的帳戶
 has_unconfirmed_mail=%s 您好,您有一封發送至( <b>%s</b>) 但未被確認的郵件。如果您未收到啟用郵件,或需要重新發送,請單擊下方的按鈕。
 resend_mail=單擊此處重新發送確認郵件
@@ -195,6 +199,7 @@ auth_failed=授權驗證失敗:%v
 
 target_branch_not_exist=目標分支不存在
 
+
 [user]
 repositories=儲存庫列表
 activity=公開活動
@@ -204,6 +209,7 @@ follow=關注
 unfollow=取消關注
 
 
+
 [settings]
 profile=個人訊息
 password=修改密碼
@@ -374,7 +380,6 @@ editor.cancel=取消
 editor.no_changes_to_show=沒有可以顯示的變更。
 
 commits.commits=次程式碼提交
-commits.find=搜尋
 commits.author=作者
 commits.message=備註
 commits.date=提交日期
@@ -480,7 +485,6 @@ issues.dependency.remove=移除成員
 pulls.new=建立合併請求
 pulls.compare_changes=建立合併請求
 pulls.filter_branch=過濾分支
-pulls.no_results=未找到結果
 pulls.create=建立合併請求
 pulls.merged_title_desc=於 %[4]s 將 %[1]d 次代碼提交從 <code>%[2]s</code>合併至 <code>%[3]s</code>
 pulls.tab_conversation=對話內容
@@ -537,7 +541,7 @@ activity.merged_prs_label=已合併
 activity.closed_issue_label=已關閉
 activity.new_issues_count_1=建立問題
 
-search=搜尋
+contributors.contribution_type.commits=提交歷史
 
 settings=儲存庫設定
 settings.desc=設定是您可以管理儲存庫設定的地方
@@ -639,6 +643,8 @@ release.downloads=下載附件
 
 
 
+[graphs]
+
 [org]
 org_name_holder=組織名稱
 org_full_name_holder=組織全名
@@ -693,6 +699,7 @@ dashboard=控制面版
 organizations=組織管理
 repositories=儲存庫管理
 config=應用設定管理
+config_settings=組織設定
 notices=系統提示管理
 monitor=應用監控面版
 first_page=首頁
@@ -755,8 +762,6 @@ repos.repo_manage_panel=儲存庫管理
 repos.owner=所有者
 repos.name=儲存庫名稱
 repos.private=私有庫
-repos.watches=關註數
-repos.stars=讚好數
 repos.issues=問題數
 repos.size=大小
 
@@ -804,7 +809,6 @@ auths.tip.oauth2_provider=OAuth2 提供者
 auths.tip.dropbox=建立新 App 在 https://www.dropbox.com/developers/apps
 auths.tip.facebook=`在 https://developers.facebook.com/apps 註冊一個新的應用,並且新增一個產品 "Facebook Login"`
 auths.tip.github=在 https://github.com/settings/applications/new 註冊一個新的 OAuth 應用程式
-auths.tip.gitlab=在 https://gitlab.com/profile/applications 註冊一個新的應用程式
 auths.tip.openid_connect=使用 OpenID 連接探索 URL (<server>/.well-known/openid-configuration) 來指定節點
 auths.delete=刪除認證來源
 auths.delete_auth_title=刪除認證來源
@@ -915,6 +919,7 @@ notices.desc=描述
 notices.op=操作
 notices.delete_success=已刪除系統提示。
 
+
 [action]
 create_repo=建立了儲存庫 <a href="%s">%s</a>
 rename_repo=重新命名儲存庫 <code>%[1]s</code> 為 <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index ea79c45674..0447a7d8b7 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -125,6 +125,15 @@ concept_user_organization=組織
 name=名稱
 value=值
 
+filter=篩選
+filter.is_archived=已封存
+filter.is_template=模板
+filter.public=公開
+filter.private=私有
+
+
+[search]
+
 [aria]
 navbar=導航列
 footer=頁尾
@@ -292,7 +301,6 @@ collaborative_repos=參與協作的儲存庫
 my_orgs=我的組織
 my_mirrors=我的鏡像
 view_home=訪問 %s
-search_repos=搜尋儲存庫...
 filter=其他篩選條件
 filter_by_team_repositories=以團隊儲存庫篩選
 feed_of=「%s」的訊息來源
@@ -313,19 +321,7 @@ issues.in_your_repos=在您的儲存庫中
 repos=儲存庫
 users=使用者
 organizations=組織
-search=搜尋
 code=程式碼
-search.type.tooltip=搜尋類型
-search.fuzzy=模糊
-search.fuzzy.tooltip=包含近似關鍵字的結果
-search.match=符合
-search.match.tooltip=只包含完全符合關鍵字的結果
-code_search_unavailable=現在無法使用程式碼搜尋。請與網站管理員聯絡。
-repo_no_results=沒有找到符合的儲存庫。
-user_no_results=沒有找到符合的使用者。
-org_no_results=沒有找到符合的組織。
-code_no_results=找不到符合您關鍵字的原始碼。
-code_search_results=「%s」的搜尋結果
 code_last_indexed_at=最後索引 %s
 relevant_repositories_tooltip=已隱藏缺少主題、圖示、說明、Fork 的儲存庫。
 relevant_repositories=只顯示相關的儲存庫,<a href="%s">顯示未篩選的結果</a>。
@@ -341,7 +337,6 @@ remember_me=記得這個裝置
 forgot_password_title=忘記密碼
 forgot_password=忘記密碼?
 sign_up_now=還沒有帳戶?馬上註冊。
-confirmation_mail_sent_prompt=新的確認信已發送至 <b>%s</b>。請在 %s內檢查您的收件匣並完成註冊作業。
 must_change_password=更新您的密碼
 allow_password_change=要求使用者更改密碼 (推薦)
 reset_password_mail_sent_prompt=確認信已發送至 <b>%s</b>。請在 %s內檢查您的收件匣並完成帳戶救援作業。
@@ -555,6 +550,7 @@ org_still_own_packages=此組織仍然擁有一個以上的套件,請先刪除
 
 target_branch_not_exist=目標分支不存在
 
+
 [user]
 change_avatar=更改大頭貼...
 repositories=儲存庫
@@ -577,6 +573,7 @@ form.name_reserved=「%s」是保留的帳號。
 form.name_pattern_not_allowed=帳號不可包含字元「%s」。
 form.name_chars_not_allowed=帳號「%s」包含無效字元。
 
+
 [settings]
 profile=個人資料
 account=帳戶
@@ -706,7 +703,6 @@ gpg_invalid_token_signature=提供的 GPG 金鑰、簽署、Token 不符合或 T
 gpg_token_required=您必須為下列的 Token 提供簽署
 gpg_token=Token
 gpg_token_help=您可以使用以下方法產生簽署:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
 gpg_token_signature=Armored GPG 簽署
 key_signature_gpg_placeholder=以「-----BEGIN PGP SIGNATURE-----」開頭
 verify_gpg_key_success=已驗證 GPG 金鑰「%s」。
@@ -866,7 +862,6 @@ already_forked=您已經 fork 過 %s
 fork_to_different_account=Fork 到其他帳戶
 fork_visibility_helper=無法更改 fork 儲存庫的瀏覽權限。
 use_template=使用此範本
-clone_in_vsc=在 VS Code 中 Clone
 download_zip=下載 ZIP
 download_tar=下載 TAR.GZ
 download_bundle=下載 BUNDLE
@@ -1154,9 +1149,7 @@ commits.desc=瀏覽原始碼修改歷程。
 commits.commits=次程式碼提交
 commits.no_commits=沒有共同的提交。「%s」和「%s」的歷史完全不同。
 commits.nothing_to_compare=這些分支是相同的。
-commits.search=搜尋提交歷史...
 commits.search.tooltip=你可以用「author:」、「committer:」、「after:」、「before:」等作為關鍵字的前綴,例如: 「revert author:Alice before:2019-01-13」。
-commits.find=搜尋
 commits.search_all=所有分支
 commits.author=作者
 commits.message=備註
@@ -1206,7 +1199,6 @@ projects.type.basic_kanban=基本看板
 projects.type.bug_triage=Bug 檢傷分類
 projects.template.desc=範本
 projects.template.desc_helper=選擇專案範本以開始
-projects.type.uncategorized=未分類
 projects.column.edit=編輯欄位
 projects.column.edit_title=名稱
 projects.column.new_title=名稱
@@ -1215,7 +1207,6 @@ projects.column.new=新增欄位
 projects.column.set_default=設為預設
 projects.column.set_default_desc=將此欄位設定為未分類問題及合併請求的預設預設值
 projects.column.delete=刪除欄位
-projects.column.deletion_desc=刪除專案欄位會將所有相關的問題移動到「未分類」,是否繼續?
 projects.column.color=顏色
 projects.open=開啟
 projects.close=關閉
@@ -1551,7 +1542,6 @@ pulls.compare_compare=拉取自
 pulls.switch_comparison_type=切換比較類型
 pulls.switch_head_and_base=切換 head 和 base
 pulls.filter_branch=過濾分支
-pulls.no_results=未找到結果
 pulls.nothing_to_compare=這些分支的內容相同,無需建立合併請求。
 pulls.nothing_to_compare_and_allow_empty_pr=這些分支的內容相同,此合併請求將會是空白的。
 pulls.has_pull_request=`已有介於這些分支間的合併請求:<a href="%[1]s">%[2]s#%[3]d</a>`
@@ -1776,16 +1766,7 @@ activity.git_stats_and_deletions=和
 activity.git_stats_deletion_1=刪除 %d 行
 activity.git_stats_deletion_n=刪除 %d 行
 
-search=搜尋
-search.search_repo=搜尋儲存庫
-search.type.tooltip=搜尋類型
-search.fuzzy=模糊
-search.fuzzy.tooltip=包含近似關鍵字的結果
-search.match=符合
-search.match.tooltip=只包含完全符合關鍵字的結果
-search.results=在 <a href="%s"> %s </a> 中搜尋 "%s" 的结果
-search.code_no_results=找不到符合您關鍵字的原始碼。
-search.code_search_unavailable=現在無法使用程式碼搜尋。請與網站管理員聯絡。
+contributors.contribution_type.commits=提交歷史
 
 settings=設定
 settings.desc=設定是您可以管理儲存庫設定的地方
@@ -1847,6 +1828,7 @@ settings.pulls.default_allow_edits_from_maintainers=預設允許維護者進行
 settings.releases_desc=啟用儲存庫版本發佈
 settings.packages_desc=啟用儲存庫套件註冊中心
 settings.projects_desc=啟用儲存庫專案
+settings.projects_mode_all=所有專案
 settings.actions_desc=啟用儲存庫 Actions
 settings.admin_settings=管理員設定
 settings.admin_enable_health_check=啟用儲存庫的健康檢查 (git fsck)
@@ -1919,7 +1901,6 @@ settings.delete_collaborator=移除
 settings.collaborator_deletion=移除協作者
 settings.collaborator_deletion_desc=移除協作者將拒絕他存取此儲存庫。是否繼續?
 settings.remove_collaborator_success=已移除協作者。
-settings.search_user_placeholder=搜尋使用者...
 settings.org_not_allowed_to_be_collaborator=不可加入組織為協作者。
 settings.change_team_access_not_allowed=只有組織擁有者可修改團隊的儲存庫存取權限
 settings.team_not_in_organization=團隊和儲存庫不在相同的組織內
@@ -1927,7 +1908,6 @@ settings.teams=團隊
 settings.add_team=增加團隊
 settings.add_team_duplicate=團隊已擁有該儲存庫
 settings.add_team_success=團隊現在可存取該儲存庫了。
-settings.search_team=搜尋團隊...
 settings.change_team_permission_tip=團隊權限可於團隊設定頁面修改,不能針對儲存庫分別調整。
 settings.delete_team_tip=此團隊可存取所有儲存庫,無法移除
 settings.remove_team_success=已移除團隊存取儲存庫的權限。
@@ -2074,9 +2054,7 @@ settings.protect_whitelist_committers=使用白名單控管推送
 settings.protect_whitelist_committers_desc=僅允許白名單內的使用者或團隊推送至該分支(但不可使用force push)。
 settings.protect_whitelist_deploy_keys=將擁有寫入權限的部署金鑰加入白名單。
 settings.protect_whitelist_users=允許推送的使用者:
-settings.protect_whitelist_search_users=搜尋使用者...
 settings.protect_whitelist_teams=允許推送的團隊:
-settings.protect_whitelist_search_teams=搜尋團隊...
 settings.protect_merge_whitelist_committers=啟用合併白名單
 settings.protect_merge_whitelist_committers_desc=僅允許白名單內的使用者或團隊將合併請求合併至該分支。
 settings.protect_merge_whitelist_users=允許合併的使用者:
@@ -2321,6 +2299,8 @@ error.csv.too_large=無法渲染此檔案,因為它太大了。
 error.csv.unexpected=無法渲染此檔案,因為它包含了未預期的字元,於第 %d 行第 %d 列。
 error.csv.invalid_field_count=無法渲染此檔案,因為它第 %d 行的欄位數量有誤。
 
+[graphs]
+
 [org]
 org_name_holder=組織名稱
 org_full_name_holder=組織全名
@@ -2422,7 +2402,6 @@ teams.write_permission_desc=這個團隊擁有<strong>寫入</strong> 權限:
 teams.admin_permission_desc=這個團隊擁有<strong>管理員</strong> 權限:成員可以讀取、推送和增加協作者到儲存庫。
 teams.create_repo_permission_desc=此外,這個團隊還擁有<strong>建立儲存庫</strong>的權限:成員可以在組織中新增儲存庫。
 teams.repositories=團隊儲存庫
-teams.search_repo_placeholder=搜尋儲存庫...
 teams.remove_all_repos_title=移除所有團隊儲存庫
 teams.remove_all_repos_desc=這將從團隊中移除所有儲存庫。
 teams.add_all_repos_title=增加所有儲存庫
@@ -2450,6 +2429,8 @@ hooks=Webhook
 authentication=認證來源
 emails=使用者電子信箱
 config=組態
+config_summary=摘要
+config_settings=設定
 notices=系統提示
 monitor=應用監控面版
 first_page=首頁
@@ -2458,7 +2439,6 @@ total=總計:%d
 
 dashboard.new_version_hint=現已推出 Gitea %s,您正在執行 %s。詳情請參閱<a target="_blank" rel="noreferrer" href="https://blog.gitea.io">部落格</a>的說明。
 dashboard.statistic=摘要
-dashboard.operations=維護作業
 dashboard.system_status=系統狀態
 dashboard.operation_name=作業名稱
 dashboard.operation_switch=開關
@@ -2611,9 +2591,6 @@ repos.unadopted.no_more=找不到其他未接管的儲存庫
 repos.owner=擁有者
 repos.name=名稱
 repos.private=私有
-repos.watches=關注數
-repos.stars=星號數
-repos.forks=Fork 數
 repos.issues=問題數
 repos.size=大小
 
@@ -2732,7 +2709,6 @@ auths.tip.nextcloud=在您的執行個體中,於選單「設定 -> 安全性 -
 auths.tip.dropbox=建立新的 App。網址:https://www.dropbox.com/developers/apps
 auths.tip.facebook=註冊新的應用程式並新增產品「Facebook 登入」。網址:https://developers.facebook.com/apps
 auths.tip.github=註冊新的 OAuth 應用程式。網址:https://github.com/settings/applications/new
-auths.tip.gitlab=註冊新的應用程式。網址:https://gitlab.com/profile/applications
 auths.tip.google_plus=從 Google API 控制台取得 OAuth2 用戶端憑證。網址:https://console.developers.google.com/
 auths.tip.openid_connect=使用 OpenID 連接探索 URL (<server>/.well-known/openid-configuration) 來指定節點
 auths.tip.twitter=建立應用程式並確保有啟用「Allow this application to be used to Sign in with Twitter」。網址:https://dev.twitter.com/apps
@@ -2933,6 +2909,7 @@ notices.desc=描述
 notices.op=操作
 notices.delete_success=已刪除系統提示。
 
+
 [action]
 create_repo=建立了儲存庫 <a href="%s">%s</a>
 rename_repo=重新命名儲存庫 <code>%[1]s</code> 為 <a href="%[2]s">%[3]s</a>
@@ -3109,6 +3086,8 @@ pypi.requires=需要 Python
 pypi.install=執行下列命令以使用 pip 安裝此套件:
 rpm.registry=透過下列命令設定此註冊中心:
 rpm.install=執行下列命令安裝此套件:
+rpm.repository=儲存庫資訊
+rpm.repository.architectures=架構
 rubygems.install=執行下列命令以使用 gem 安裝此套件:
 rubygems.install2=或將它加到 Gemfile:
 rubygems.dependencies.runtime=執行階段相依性
diff --git a/package-lock.json b/package-lock.json
index 6918dc64b7..35bf886fc8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,85 +5,97 @@
   "packages": {
     "": {
       "dependencies": {
-        "@citation-js/core": "0.7.6",
-        "@citation-js/plugin-bibtex": "0.7.8",
-        "@citation-js/plugin-csl": "0.7.6",
+        "@citation-js/core": "0.7.9",
+        "@citation-js/plugin-bibtex": "0.7.9",
+        "@citation-js/plugin-csl": "0.7.9",
         "@citation-js/plugin-software-formats": "0.6.1",
-        "@claviska/jquery-minicolors": "2.3.6",
-        "@github/markdown-toolbar-element": "2.2.1",
-        "@github/relative-time-element": "4.3.1",
+        "@github/markdown-toolbar-element": "2.2.3",
+        "@github/relative-time-element": "4.4.0",
         "@github/text-expander-element": "2.6.1",
         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
-        "@primer/octicons": "19.8.0",
-        "@webcomponents/custom-elements": "1.6.0",
+        "@primer/octicons": "19.9.0",
         "add-asset-webpack-plugin": "2.0.1",
         "ansi_up": "6.0.2",
-        "asciinema-player": "3.6.3",
-        "clippie": "4.0.6",
-        "css-loader": "6.10.0",
+        "asciinema-player": "3.7.1",
+        "chart.js": "4.4.2",
+        "chartjs-adapter-dayjs-4": "1.0.4",
+        "chartjs-plugin-zoom": "2.0.1",
+        "clippie": "4.0.7",
+        "css-loader": "7.0.0",
+        "dayjs": "1.11.10",
         "dropzone": "6.0.0-beta.2",
         "easymde": "2.18.0",
-        "esbuild-loader": "4.0.3",
+        "esbuild-loader": "4.1.0",
         "escape-goat": "4.0.0",
         "fast-glob": "3.3.2",
-        "htmx.org": "1.9.10",
+        "htmx.org": "1.9.11",
+        "idiomorph": "0.3.0",
         "jquery": "3.7.1",
-        "katex": "0.16.9",
+        "katex": "0.16.10",
         "license-checker-webpack-plugin": "0.2.1",
-        "mermaid": "10.7.0",
-        "mini-css-extract-plugin": "2.8.0",
-        "minimatch": "9.0.3",
-        "monaco-editor": "0.45.0",
+        "mermaid": "10.9.0",
+        "mini-css-extract-plugin": "2.8.1",
+        "minimatch": "9.0.4",
+        "monaco-editor": "0.47.0",
         "monaco-editor-webpack-plugin": "7.1.0",
-        "pdfobject": "2.2.12",
+        "pdfobject": "2.3.0",
+        "postcss": "8.4.38",
+        "postcss-loader": "8.1.1",
+        "postcss-nesting": "12.1.1",
         "pretty-ms": "9.0.0",
         "sortablejs": "1.15.2",
-        "swagger-ui-dist": "5.11.2",
+        "swagger-ui-dist": "5.13.0",
+        "tailwindcss": "3.4.3",
+        "temporal-polyfill": "0.2.3",
         "throttle-debounce": "5.0.0",
         "tinycolor2": "1.6.0",
         "tippy.js": "6.3.7",
         "toastify-js": "1.12.0",
         "tributejs": "5.1.3",
         "uint8-to-base64": "0.2.0",
-        "vue": "3.4.15",
+        "vanilla-colorful": "0.7.2",
+        "vue": "3.4.21",
         "vue-bar-graph": "2.0.0",
+        "vue-chartjs": "5.3.0",
         "vue-loader": "17.4.2",
         "vue3-calendar-heatmap": "2.0.5",
-        "webpack": "5.90.1",
+        "webpack": "5.91.0",
         "webpack-cli": "5.1.4",
         "wrap-ansi": "9.0.0"
       },
       "devDependencies": {
         "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
-        "@playwright/test": "1.41.1",
-        "@stoplight/spectral-cli": "6.11.0",
-        "@stylistic/eslint-plugin-js": "1.5.4",
-        "@stylistic/stylelint-plugin": "2.0.0",
-        "@vitejs/plugin-vue": "5.0.3",
-        "eslint": "8.56.0",
+        "@playwright/test": "1.42.1",
+        "@stoplight/spectral-cli": "6.11.1",
+        "@stylistic/eslint-plugin-js": "1.7.0",
+        "@stylistic/stylelint-plugin": "2.1.1",
+        "@vitejs/plugin-vue": "5.0.4",
+        "eslint": "8.57.0",
         "eslint-plugin-array-func": "4.0.0",
+        "eslint-plugin-github": "4.10.2",
         "eslint-plugin-i": "2.29.1",
         "eslint-plugin-jquery": "1.5.1",
         "eslint-plugin-no-jquery": "2.7.0",
         "eslint-plugin-no-use-extend-native": "0.5.0",
-        "eslint-plugin-regexp": "2.2.0",
-        "eslint-plugin-sonarjs": "0.23.0",
-        "eslint-plugin-unicorn": "50.0.1",
-        "eslint-plugin-vitest": "0.3.21",
-        "eslint-plugin-vitest-globals": "1.4.0",
-        "eslint-plugin-vue": "9.21.1",
-        "eslint-plugin-vue-scoped-css": "2.7.2",
+        "eslint-plugin-regexp": "2.4.0",
+        "eslint-plugin-sonarjs": "0.25.1",
+        "eslint-plugin-unicorn": "52.0.0",
+        "eslint-plugin-vitest": "0.4.1",
+        "eslint-plugin-vitest-globals": "1.5.0",
+        "eslint-plugin-vue": "9.24.0",
+        "eslint-plugin-vue-scoped-css": "2.8.0",
         "eslint-plugin-wc": "2.0.4",
-        "jsdom": "24.0.0",
+        "happy-dom": "14.5.0",
         "markdownlint-cli": "0.39.0",
         "postcss-html": "1.6.0",
-        "stylelint": "16.2.1",
+        "stylelint": "16.3.1",
         "stylelint-declaration-block-no-ignored-properties": "2.8.0",
         "stylelint-declaration-strict-value": "1.10.4",
+        "stylelint-value-no-unknown-custom-properties": "6.0.1",
         "svgo": "3.2.0",
-        "updates": "15.1.1",
-        "vite-string-plugin": "1.1.3",
-        "vitest": "1.2.2"
+        "updates": "16.0.0",
+        "vite-string-plugin": "1.1.5",
+        "vitest": "1.4.0"
       },
       "engines": {
         "node": ">= 18.0.0"
@@ -98,6 +110,17 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/@asyncapi/specs": {
       "version": "4.3.1",
       "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-4.3.1.tgz",
@@ -108,107 +131,34 @@
       }
     },
     "node_modules/@babel/code-frame": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
-      "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
-      "dev": true,
+      "version": "7.24.2",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
+      "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
       "dependencies": {
-        "@babel/highlight": "^7.23.4",
-        "chalk": "^2.4.2"
+        "@babel/highlight": "^7.24.2",
+        "picocolors": "^1.0.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/code-frame/node_modules/ansi-styles": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
-      "dependencies": {
-        "color-convert": "^1.9.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/chalk": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^3.2.1",
-        "escape-string-regexp": "^1.0.5",
-        "supports-color": "^5.3.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/color-convert": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
-      "dependencies": {
-        "color-name": "1.1.3"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "dev": true
-    },
-    "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.8.0"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/has-flag": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/code-frame/node_modules/supports-color": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dev": true,
-      "dependencies": {
-        "has-flag": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/@babel/helper-validator-identifier": {
       "version": "7.22.20",
       "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
       "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
-      "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/highlight": {
-      "version": "7.23.4",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
-      "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
-      "dev": true,
+      "version": "7.24.2",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
+      "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
       "dependencies": {
         "@babel/helper-validator-identifier": "^7.22.20",
         "chalk": "^2.4.2",
-        "js-tokens": "^4.0.0"
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.0.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -218,7 +168,6 @@
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
       "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
       "dependencies": {
         "color-convert": "^1.9.0"
       },
@@ -230,7 +179,6 @@
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
       "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^3.2.1",
         "escape-string-regexp": "^1.0.5",
@@ -244,7 +192,6 @@
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
       "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
       "dependencies": {
         "color-name": "1.1.3"
       }
@@ -252,14 +199,12 @@
     "node_modules/@babel/highlight/node_modules/color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "dev": true
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
     },
     "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
       "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "dev": true,
       "engines": {
         "node": ">=0.8.0"
       }
@@ -268,7 +213,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
       "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "dev": true,
       "engines": {
         "node": ">=4"
       }
@@ -276,14 +220,12 @@
     "node_modules/@babel/highlight/node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-      "dev": true
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
     },
     "node_modules/@babel/highlight/node_modules/supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
       "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dev": true,
       "dependencies": {
         "has-flag": "^3.0.0"
       },
@@ -292,9 +234,9 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.23.9",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
-      "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
+      "version": "7.24.4",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz",
+      "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==",
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -303,9 +245,9 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.23.9",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
-      "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
+      "version": "7.24.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz",
+      "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==",
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -319,9 +261,9 @@
       "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A=="
     },
     "node_modules/@citation-js/core": {
-      "version": "0.7.6",
-      "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.6.tgz",
-      "integrity": "sha512-qbB6RjwSsx/AjlCSAqoWKN05VxpjADYe8GmnPJnRB7QeNiVmqaRc8NSQDdvQ+4qhCkQOtMH15Sa2Nde4cvlXhw==",
+      "version": "0.7.9",
+      "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.9.tgz",
+      "integrity": "sha512-fSbkB32JayDChZnAYC/kB+sWHRvxxL7ibVetyBOyzOc+5aCnjb6UVsbcfhnkOIEyAMoRRvWDyFmakEoTtA5ttQ==",
       "dependencies": {
         "@citation-js/date": "^0.5.0",
         "@citation-js/name": "^0.4.2",
@@ -349,9 +291,9 @@
       }
     },
     "node_modules/@citation-js/plugin-bibtex": {
-      "version": "0.7.8",
-      "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.8.tgz",
-      "integrity": "sha512-20fUXe1zm1oCONFflGj3mgIk6DHspPjWrBirGfsyHmVSR/4xqnSbrqtztLiV15zt3tbKLepTaHm3ZTrcLOK0MA==",
+      "version": "0.7.9",
+      "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.9.tgz",
+      "integrity": "sha512-gIJpCd6vmmTOcRfDrSOjtoNhw2Mi94UwFxmgJ7GwkXyTYcNheW5VlMMo1tlqjakJGARQ0eOsKcI57gSPqJSS2g==",
       "dependencies": {
         "@citation-js/date": "^0.5.0",
         "@citation-js/name": "^0.4.2",
@@ -377,9 +319,9 @@
       }
     },
     "node_modules/@citation-js/plugin-csl": {
-      "version": "0.7.6",
-      "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.6.tgz",
-      "integrity": "sha512-H/dhzU56+D71Hzjto1x9PDtvsWaiI+Dx6Jj1vjiFtCCnbU/Zvqo5xFZNPstee+hFE6AsJ2xYlI8QujrGH+V1pQ==",
+      "version": "0.7.9",
+      "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.9.tgz",
+      "integrity": "sha512-mbD7CnUiPOuVnjeJwo+d0RGUcY0PE8n01gHyjq0qpTeS42EGmQ9+LzqfsTUVWWBndTwc6zLRuIF1qFAUHKE4oA==",
       "dependencies": {
         "@citation-js/date": "^0.5.0",
         "citeproc": "^2.4.6"
@@ -453,18 +395,10 @@
         "node": ">=14.0.0"
       }
     },
-    "node_modules/@claviska/jquery-minicolors": {
-      "version": "2.3.6",
-      "resolved": "https://registry.npmjs.org/@claviska/jquery-minicolors/-/jquery-minicolors-2.3.6.tgz",
-      "integrity": "sha512-8Ro6D4GCrmOl41+6w4NFhEOpx8vjxwVRI69bulXsFDt49uVRKhLU5TnzEV7AmOJrylkVq+ugnYNMiGHBieeKUQ==",
-      "peerDependencies": {
-        "jquery": ">= 1.7.x"
-      }
-    },
     "node_modules/@csstools/css-parser-algorithms": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz",
-      "integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==",
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz",
+      "integrity": "sha512-ubEkAaTfVZa+WwGhs5jbo5Xfqpeaybr/RvWzvFxRs4jfq16wH8l8Ty/QEEpINxll4xhuGfdMbipRyz5QZh9+FA==",
       "dev": true,
       "funding": [
         {
@@ -480,13 +414,13 @@
         "node": "^14 || ^16 || >=18"
       },
       "peerDependencies": {
-        "@csstools/css-tokenizer": "^2.2.3"
+        "@csstools/css-tokenizer": "^2.2.4"
       }
     },
     "node_modules/@csstools/css-tokenizer": {
-      "version": "2.2.3",
-      "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz",
-      "integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==",
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.4.tgz",
+      "integrity": "sha512-PuWRAewQLbDhGeTvFuq2oClaSCKPIBmHyIobCV39JHRYN0byDcUWJl5baPeNUcqrjtdMNqFooE0FGl31I3JOqw==",
       "dev": true,
       "funding": [
         {
@@ -503,9 +437,9 @@
       }
     },
     "node_modules/@csstools/media-query-list-parser": {
-      "version": "2.1.7",
-      "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.7.tgz",
-      "integrity": "sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ==",
+      "version": "2.1.9",
+      "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.9.tgz",
+      "integrity": "sha512-qqGuFfbn4rUmyOB0u8CVISIp5FfJ5GAR3mBrZ9/TKndHakdnm6pY0L/fbLcpPnrzwCyyTEZl1nUcXAYHEWneTA==",
       "dev": true,
       "funding": [
         {
@@ -521,15 +455,35 @@
         "node": "^14 || ^16 || >=18"
       },
       "peerDependencies": {
-        "@csstools/css-parser-algorithms": "^2.5.0",
-        "@csstools/css-tokenizer": "^2.2.3"
+        "@csstools/css-parser-algorithms": "^2.6.1",
+        "@csstools/css-tokenizer": "^2.2.4"
+      }
+    },
+    "node_modules/@csstools/selector-resolve-nested": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-1.1.0.tgz",
+      "integrity": "sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "engines": {
+        "node": "^14 || ^16 || >=18"
+      },
+      "peerDependencies": {
+        "postcss-selector-parser": "^6.0.13"
       }
     },
     "node_modules/@csstools/selector-specificity": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.1.tgz",
-      "integrity": "sha512-NPljRHkq4a14YzZ3YD406uaxh7s0g6eAq3L9aLOWywoqe8PkYamAvtsh7KNX6c++ihDrJ0RiU+/z7rGnhlZ5ww==",
-      "dev": true,
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.3.tgz",
+      "integrity": "sha512-KEPNw4+WW5AVEIyzC80rTbWEUatTW2lXpN8+8ILC8PiPeWPjwUzrPZDIOZ2wwqDmeqOYTdSGyL3+vE5GC3FB3Q==",
       "funding": [
         {
           "type": "github",
@@ -555,10 +509,20 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/@dual-bundle/import-meta-resolve": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz",
+      "integrity": "sha512-ZKXyJeFAzcpKM2kk8ipoGIPUqx9BX52omTGnfwjJvxOCaZTM2wtDK7zN0aIgPRbT9XYAlha0HtmZ+XKteuh0Gw==",
+      "dev": true,
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
     "node_modules/@esbuild/aix-ppc64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
-      "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
+      "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
       "cpu": [
         "ppc64"
       ],
@@ -571,9 +535,9 @@
       }
     },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
-      "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
+      "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
       "cpu": [
         "arm"
       ],
@@ -586,9 +550,9 @@
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
-      "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
+      "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
       "cpu": [
         "arm64"
       ],
@@ -601,9 +565,9 @@
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
-      "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
+      "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
       "cpu": [
         "x64"
       ],
@@ -616,9 +580,9 @@
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
-      "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
+      "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
       "cpu": [
         "arm64"
       ],
@@ -631,9 +595,9 @@
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
-      "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
+      "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
       "cpu": [
         "x64"
       ],
@@ -646,9 +610,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
-      "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
+      "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
       "cpu": [
         "arm64"
       ],
@@ -661,9 +625,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
-      "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
+      "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
       "cpu": [
         "x64"
       ],
@@ -676,9 +640,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
-      "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
+      "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
       "cpu": [
         "arm"
       ],
@@ -691,9 +655,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
-      "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
+      "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
       "cpu": [
         "arm64"
       ],
@@ -706,9 +670,9 @@
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
-      "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
+      "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
       "cpu": [
         "ia32"
       ],
@@ -721,9 +685,9 @@
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
-      "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
+      "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
       "cpu": [
         "loong64"
       ],
@@ -736,9 +700,9 @@
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
-      "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
+      "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
       "cpu": [
         "mips64el"
       ],
@@ -751,9 +715,9 @@
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
-      "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
+      "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
       "cpu": [
         "ppc64"
       ],
@@ -766,9 +730,9 @@
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
-      "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
+      "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
       "cpu": [
         "riscv64"
       ],
@@ -781,9 +745,9 @@
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
-      "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
+      "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
       "cpu": [
         "s390x"
       ],
@@ -796,9 +760,9 @@
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
-      "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
+      "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
       "cpu": [
         "x64"
       ],
@@ -811,9 +775,9 @@
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
-      "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
+      "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
       "cpu": [
         "x64"
       ],
@@ -826,9 +790,9 @@
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
-      "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
+      "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
       "cpu": [
         "x64"
       ],
@@ -841,9 +805,9 @@
       }
     },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
-      "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
+      "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
       "cpu": [
         "x64"
       ],
@@ -856,9 +820,9 @@
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
-      "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
+      "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
       "cpu": [
         "arm64"
       ],
@@ -871,9 +835,9 @@
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
-      "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
+      "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
       "cpu": [
         "ia32"
       ],
@@ -886,9 +850,9 @@
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
-      "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
+      "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
       "cpu": [
         "x64"
       ],
@@ -1008,28 +972,34 @@
       }
     },
     "node_modules/@eslint/js": {
-      "version": "8.56.0",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
-      "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
+      "version": "8.57.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
+      "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/@github/browserslist-config": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@github/browserslist-config/-/browserslist-config-1.0.0.tgz",
+      "integrity": "sha512-gIhjdJp/c2beaIWWIlsXdqXVRUz3r2BxBCpfz/F3JXHvSAQ1paMYjLH+maEATtENg+k5eLV7gA+9yPp762ieuw==",
+      "dev": true
+    },
     "node_modules/@github/combobox-nav": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.3.1.tgz",
       "integrity": "sha512-gwxPzLw8XKecy1nP63i9lOBritS3bWmxl02UX6G0TwMQZbMem1BCS1tEZgYd3mkrkiDrUMWaX+DbFCuDFo3K+A=="
     },
     "node_modules/@github/markdown-toolbar-element": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.2.1.tgz",
-      "integrity": "sha512-ap+ulyqzG3aVqwKsKjbDdYwM75TQXZpPtmIuPwm+54OTgcC96267oX3cEqd1wSqGsH7O5PonZ//fE9jH7Q4JkA=="
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.2.3.tgz",
+      "integrity": "sha512-AlquKGee+IWiAMYVB0xyHFZRMnu4n3X4HTvJHu79GiVJ1ojTukCWyxMlF5NMsecoLcBKsuBhx3QPv2vkE/zQ0A=="
     },
     "node_modules/@github/relative-time-element": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.3.1.tgz",
-      "integrity": "sha512-zL79nlhZVCg7x2Pf/HT5MB0mowmErE71VXpF10/3Wy8dQwkninNO1M9aOizh2wKC5LkSpDXqNYjDZwbH0/bcSg=="
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.0.tgz",
+      "integrity": "sha512-CrI6oAecoahG7PF5dsgjdvlF5kCtusVMjg810EULD81TvnDsP+k/FRi/ClFubWLgBo4EGpr2EfvmumtqQFo7ow=="
     },
     "node_modules/@github/text-expander-element": {
       "version": "2.6.1",
@@ -1089,16 +1059,15 @@
       }
     },
     "node_modules/@humanwhocodes/object-schema": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
-      "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+      "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
       "dev": true
     },
     "node_modules/@isaacs/cliui": {
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
       "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
-      "dev": true,
       "dependencies": {
         "string-width": "^5.1.2",
         "string-width-cjs": "npm:string-width@^4.2.0",
@@ -1115,7 +1084,6 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
       "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
-      "dev": true,
       "engines": {
         "node": ">=12"
       },
@@ -1127,7 +1095,6 @@
       "version": "6.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
       "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
-      "dev": true,
       "engines": {
         "node": ">=12"
       },
@@ -1135,17 +1102,10 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
-    "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
-      "version": "9.2.2",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
-      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
-      "dev": true
-    },
     "node_modules/@isaacs/cliui/node_modules/string-width": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
       "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
-      "dev": true,
       "dependencies": {
         "eastasianwidth": "^0.2.0",
         "emoji-regex": "^9.2.2",
@@ -1162,7 +1122,6 @@
       "version": "7.1.0",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
       "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-      "dev": true,
       "dependencies": {
         "ansi-regex": "^6.0.1"
       },
@@ -1177,7 +1136,6 @@
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
       "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^6.1.0",
         "string-width": "^5.0.1",
@@ -1203,41 +1161,41 @@
       }
     },
     "node_modules/@jridgewell/gen-mapping": {
-      "version": "0.3.3",
-      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
-      "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
+      "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
       "dependencies": {
-        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/set-array": "^1.2.1",
         "@jridgewell/sourcemap-codec": "^1.4.10",
-        "@jridgewell/trace-mapping": "^0.3.9"
+        "@jridgewell/trace-mapping": "^0.3.24"
       },
       "engines": {
         "node": ">=6.0.0"
       }
     },
     "node_modules/@jridgewell/resolve-uri": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
-      "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
       "engines": {
         "node": ">=6.0.0"
       }
     },
     "node_modules/@jridgewell/set-array": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
-      "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+      "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
       "engines": {
         "node": ">=6.0.0"
       }
     },
     "node_modules/@jridgewell/source-map": {
-      "version": "0.3.5",
-      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
-      "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
+      "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
       "dependencies": {
-        "@jridgewell/gen-mapping": "^0.3.0",
-        "@jridgewell/trace-mapping": "^0.3.9"
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.25"
       }
     },
     "node_modules/@jridgewell/sourcemap-codec": {
@@ -1246,9 +1204,9 @@
       "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
     },
     "node_modules/@jridgewell/trace-mapping": {
-      "version": "0.3.22",
-      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
-      "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
+      "version": "0.3.25",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+      "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
       "dependencies": {
         "@jridgewell/resolve-uri": "^3.1.0",
         "@jridgewell/sourcemap-codec": "^1.4.14"
@@ -1278,6 +1236,11 @@
         "jsep": "^0.4.0||^1.0.0"
       }
     },
+    "node_modules/@kurkle/color": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
+      "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
+    },
     "node_modules/@mcaptcha/core-glue": {
       "version": "0.1.0-alpha-5",
       "resolved": "https://registry.npmjs.org/@mcaptcha/core-glue/-/core-glue-0.1.0-alpha-5.tgz",
@@ -1363,19 +1326,30 @@
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
       "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
-      "dev": true,
       "optional": true,
       "engines": {
         "node": ">=14"
       }
     },
+    "node_modules/@pkgr/core": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
+      "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
+      "dev": true,
+      "engines": {
+        "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/unts"
+      }
+    },
     "node_modules/@playwright/test": {
-      "version": "1.41.1",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.1.tgz",
-      "integrity": "sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==",
+      "version": "1.42.1",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
+      "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
       "dev": true,
       "dependencies": {
-        "playwright": "1.41.1"
+        "playwright": "1.42.1"
       },
       "bin": {
         "playwright": "cli.js"
@@ -1394,9 +1368,9 @@
       }
     },
     "node_modules/@primer/octicons": {
-      "version": "19.8.0",
-      "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.8.0.tgz",
-      "integrity": "sha512-Imze/fyW41Io5fN+27T5EAeXJrgBjMbz6nzU+wYbRylXvIAjLPUvaJPVoStiFlgSU+TjTUJqg5A9rgMDzTyMCg==",
+      "version": "19.9.0",
+      "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.9.0.tgz",
+      "integrity": "sha512-uAZa9cMgWkzbEsZnYWB7tg0vt7QprubD7ljtprz2fBJ8CjyqoxFRRsFvH4UiJdjK/3o87ODgDkhiflyJXDh+Lg==",
       "dependencies": {
         "object-assign": "^4.1.1"
       }
@@ -1446,9 +1420,9 @@
       "dev": true
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz",
-      "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.0.tgz",
+      "integrity": "sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==",
       "cpu": [
         "arm"
       ],
@@ -1459,9 +1433,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz",
-      "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.0.tgz",
+      "integrity": "sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==",
       "cpu": [
         "arm64"
       ],
@@ -1472,9 +1446,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz",
-      "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.0.tgz",
+      "integrity": "sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==",
       "cpu": [
         "arm64"
       ],
@@ -1485,9 +1459,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz",
-      "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.0.tgz",
+      "integrity": "sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==",
       "cpu": [
         "x64"
       ],
@@ -1498,9 +1472,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz",
-      "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.0.tgz",
+      "integrity": "sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==",
       "cpu": [
         "arm"
       ],
@@ -1511,9 +1485,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz",
-      "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.0.tgz",
+      "integrity": "sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==",
       "cpu": [
         "arm64"
       ],
@@ -1524,9 +1498,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz",
-      "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.0.tgz",
+      "integrity": "sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==",
       "cpu": [
         "arm64"
       ],
@@ -1536,10 +1510,23 @@
         "linux"
       ]
     },
+    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.0.tgz",
+      "integrity": "sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==",
+      "cpu": [
+        "ppc64le"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz",
-      "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.0.tgz",
+      "integrity": "sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==",
       "cpu": [
         "riscv64"
       ],
@@ -1549,10 +1536,23 @@
         "linux"
       ]
     },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.0.tgz",
+      "integrity": "sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz",
-      "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.0.tgz",
+      "integrity": "sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==",
       "cpu": [
         "x64"
       ],
@@ -1563,9 +1563,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz",
-      "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.0.tgz",
+      "integrity": "sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==",
       "cpu": [
         "x64"
       ],
@@ -1576,9 +1576,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz",
-      "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.0.tgz",
+      "integrity": "sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==",
       "cpu": [
         "arm64"
       ],
@@ -1589,9 +1589,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz",
-      "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.0.tgz",
+      "integrity": "sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==",
       "cpu": [
         "ia32"
       ],
@@ -1602,9 +1602,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz",
-      "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.0.tgz",
+      "integrity": "sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==",
       "cpu": [
         "x64"
       ],
@@ -1712,9 +1712,9 @@
       }
     },
     "node_modules/@stoplight/spectral-cli": {
-      "version": "6.11.0",
-      "resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.11.0.tgz",
-      "integrity": "sha512-IURDN47BPIf3q4ZyUPujGpBzuHWFE5yT34w9rTJ1GKA4SgdscEdQO9KoTjOPT4G4cvDlEV3bNxwQ3uRm7+wRlA==",
+      "version": "6.11.1",
+      "resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.11.1.tgz",
+      "integrity": "sha512-1zqsQ0TOuVSnxxZ9mHBfC0IygV6ex7nAY6Mp59mLmw5fW103U9yPVK5ZcX9ZngCmr3PdteAnMDUIIaoDGso6nA==",
       "dev": true,
       "dependencies": {
         "@stoplight/json": "~3.21.0",
@@ -1735,7 +1735,7 @@
         "pony-cause": "^1.0.0",
         "stacktracey": "^2.1.7",
         "tslib": "^2.3.0",
-        "yargs": "17.3.1"
+        "yargs": "~17.7.2"
       },
       "bin": {
         "spectral": "dist/index.js"
@@ -1899,20 +1899,33 @@
       }
     },
     "node_modules/@stoplight/spectral-parsers": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.3.tgz",
-      "integrity": "sha512-J0KW5Rh5cHWnJQ3yN+cr/ijNFVirPSR0pkQbdrNX30VboEl083UEDrQ3yov9kjLVIWEk9t9kKE7Eo3QT/k4JLA==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.4.tgz",
+      "integrity": "sha512-nCTVvtX6q71M8o5Uvv9kxU31Gk1TRmgD6/k8HBhdCmKG6FWcwgjiZouA/R3xHLn/VwTI/9k8SdG5Mkdy0RBqbQ==",
       "dev": true,
       "dependencies": {
         "@stoplight/json": "~3.21.0",
-        "@stoplight/types": "^13.6.0",
-        "@stoplight/yaml": "~4.2.3",
+        "@stoplight/types": "^14.1.1",
+        "@stoplight/yaml": "~4.3.0",
         "tslib": "^2.3.1"
       },
       "engines": {
         "node": "^12.20 || >=14.13"
       }
     },
+    "node_modules/@stoplight/spectral-parsers/node_modules/@stoplight/types": {
+      "version": "14.1.1",
+      "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz",
+      "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.4",
+        "utility-types": "^3.10.0"
+      },
+      "engines": {
+        "node": "^12.20 || >=14.13"
+      }
+    },
     "node_modules/@stoplight/spectral-ref-resolver": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/@stoplight/spectral-ref-resolver/-/spectral-ref-resolver-1.0.4.tgz",
@@ -1981,6 +1994,27 @@
         "node": ">=12"
       }
     },
+    "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/@stoplight/yaml": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.2.3.tgz",
+      "integrity": "sha512-Mx01wjRAR9C7yLMUyYFTfbUf5DimEpHMkRDQ1PKLe9dfNILbgdxyrncsOXM3vCpsQ1Hfj4bPiGl+u4u6e9Akqw==",
+      "dev": true,
+      "dependencies": {
+        "@stoplight/ordered-object-literal": "^1.0.1",
+        "@stoplight/types": "^13.0.0",
+        "@stoplight/yaml-ast-parser": "0.0.48",
+        "tslib": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=10.8"
+      }
+    },
+    "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/@stoplight/yaml-ast-parser": {
+      "version": "0.0.48",
+      "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.48.tgz",
+      "integrity": "sha512-sV+51I7WYnLJnKPn2EMWgS4EUfoP4iWEbrWwbXsj0MZCB/xOK8j6+C9fntIdOM50kpx45ZLC3s6kwKivWuqvyg==",
+      "dev": true
+    },
     "node_modules/@stoplight/spectral-rulesets": {
       "version": "1.18.1",
       "resolved": "https://registry.npmjs.org/@stoplight/spectral-rulesets/-/spectral-rulesets-1.18.1.tgz",
@@ -2051,14 +2085,14 @@
       }
     },
     "node_modules/@stoplight/yaml": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.2.3.tgz",
-      "integrity": "sha512-Mx01wjRAR9C7yLMUyYFTfbUf5DimEpHMkRDQ1PKLe9dfNILbgdxyrncsOXM3vCpsQ1Hfj4bPiGl+u4u6e9Akqw==",
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz",
+      "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==",
       "dev": true,
       "dependencies": {
-        "@stoplight/ordered-object-literal": "^1.0.1",
-        "@stoplight/types": "^13.0.0",
-        "@stoplight/yaml-ast-parser": "0.0.48",
+        "@stoplight/ordered-object-literal": "^1.0.5",
+        "@stoplight/types": "^14.1.1",
+        "@stoplight/yaml-ast-parser": "0.0.50",
         "tslib": "^2.2.0"
       },
       "engines": {
@@ -2066,17 +2100,31 @@
       }
     },
     "node_modules/@stoplight/yaml-ast-parser": {
-      "version": "0.0.48",
-      "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.48.tgz",
-      "integrity": "sha512-sV+51I7WYnLJnKPn2EMWgS4EUfoP4iWEbrWwbXsj0MZCB/xOK8j6+C9fntIdOM50kpx45ZLC3s6kwKivWuqvyg==",
+      "version": "0.0.50",
+      "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz",
+      "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==",
       "dev": true
     },
-    "node_modules/@stylistic/eslint-plugin-js": {
-      "version": "1.5.4",
-      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.5.4.tgz",
-      "integrity": "sha512-3ctWb3NvJNV1MsrZN91cYp2EGInLPSoZKphXIbIRx/zjZxKwLDr9z4LMOWtqjq14li/OgqUUcMq5pj8fgbLoTw==",
+    "node_modules/@stoplight/yaml/node_modules/@stoplight/types": {
+      "version": "14.1.1",
+      "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz",
+      "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==",
       "dev": true,
       "dependencies": {
+        "@types/json-schema": "^7.0.4",
+        "utility-types": "^3.10.0"
+      },
+      "engines": {
+        "node": "^12.20 || >=14.13"
+      }
+    },
+    "node_modules/@stylistic/eslint-plugin-js": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.7.0.tgz",
+      "integrity": "sha512-PN6On/+or63FGnhhMKSQfYcWutRlzOiYlVdLM6yN7lquoBTqUJHYnl4TA4MHwiAt46X5gRxDr1+xPZ1lOLcL+Q==",
+      "dev": true,
+      "dependencies": {
+        "@types/eslint": "^8.56.2",
         "acorn": "^8.11.3",
         "escape-string-regexp": "^4.0.0",
         "eslint-visitor-keys": "^3.4.3",
@@ -2090,19 +2138,19 @@
       }
     },
     "node_modules/@stylistic/stylelint-plugin": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-2.0.0.tgz",
-      "integrity": "sha512-dHKuT6PGd1WGZLOTuozAM7GdQzdmlmnFXYzvV1jYJXXpcCpV/OJ3+n8TXpMkoOeKHpJydY43EOoZTO1W/FOA4Q==",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-2.1.1.tgz",
+      "integrity": "sha512-xqHTmQZN7EbnFDW7jw0rAsdFNO4IRqvXhrh3qhUlIwF/x09Zm7kgs/ADktHxsTJYcw346PpGihsB0t4pZhpeHw==",
       "dev": true,
       "dependencies": {
-        "@csstools/css-parser-algorithms": "^2.3.2",
-        "@csstools/css-tokenizer": "^2.2.1",
-        "@csstools/media-query-list-parser": "^2.1.5",
+        "@csstools/css-parser-algorithms": "^2.5.0",
+        "@csstools/css-tokenizer": "^2.2.3",
+        "@csstools/media-query-list-parser": "^2.1.7",
         "is-plain-object": "^5.0.0",
-        "postcss-selector-parser": "^6.0.13",
+        "postcss-selector-parser": "^6.0.15",
         "postcss-value-parser": "^4.2.0",
         "style-search": "^0.1.0",
-        "stylelint": "^16.0.2"
+        "stylelint": "^16.2.1"
       },
       "engines": {
         "node": "^18.12 || >=20.9"
@@ -2169,9 +2217,9 @@
       }
     },
     "node_modules/@types/eslint": {
-      "version": "8.56.2",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz",
-      "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==",
+      "version": "8.56.7",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz",
+      "integrity": "sha512-SjDvI/x3zsZnOkYZ3lCt9lOZWZLB2jIlNKz+LBgCtDurK0JZcwucxYHn1w2BJkD34dgX9Tjnak0txtq4WTggEA==",
       "dependencies": {
         "@types/estree": "*",
         "@types/json-schema": "*"
@@ -2196,6 +2244,12 @@
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
       "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
     },
+    "node_modules/@types/json5": {
+      "version": "0.0.29",
+      "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+      "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+      "dev": true
+    },
     "node_modules/@types/marked": {
       "version": "4.3.2",
       "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz",
@@ -2215,9 +2269,9 @@
       "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
     },
     "node_modules/@types/node": {
-      "version": "20.11.14",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz",
-      "integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==",
+      "version": "20.12.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.4.tgz",
+      "integrity": "sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==",
       "dependencies": {
         "undici-types": "~5.26.4"
       }
@@ -2235,9 +2289,9 @@
       "dev": true
     },
     "node_modules/@types/semver": {
-      "version": "7.5.6",
-      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
-      "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
+      "version": "7.5.8",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+      "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
       "dev": true
     },
     "node_modules/@types/tern": {
@@ -2259,30 +2313,120 @@
       "integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==",
       "dev": true
     },
-    "node_modules/@typescript-eslint/scope-manager": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
-      "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.5.0.tgz",
+      "integrity": "sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.21.0",
-        "@typescript-eslint/visitor-keys": "6.21.0"
+        "@eslint-community/regexpp": "^4.5.1",
+        "@typescript-eslint/scope-manager": "7.5.0",
+        "@typescript-eslint/type-utils": "7.5.0",
+        "@typescript-eslint/utils": "7.5.0",
+        "@typescript-eslint/visitor-keys": "7.5.0",
+        "debug": "^4.3.4",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.4",
+        "natural-compare": "^1.4.0",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^7.0.0",
+        "eslint": "^8.56.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz",
+      "integrity": "sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "7.5.0",
+        "@typescript-eslint/types": "7.5.0",
+        "@typescript-eslint/typescript-estree": "7.5.0",
+        "@typescript-eslint/visitor-keys": "7.5.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^18.18.0 || >=20.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.56.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/scope-manager": {
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz",
+      "integrity": "sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "7.5.0",
+        "@typescript-eslint/visitor-keys": "7.5.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
       }
     },
+    "node_modules/@typescript-eslint/type-utils": {
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz",
+      "integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/typescript-estree": "7.5.0",
+        "@typescript-eslint/utils": "7.5.0",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || >=20.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.56.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@typescript-eslint/types": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
-      "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz",
+      "integrity": "sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==",
       "dev": true,
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -2290,13 +2434,13 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
-      "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.5.0.tgz",
+      "integrity": "sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.21.0",
-        "@typescript-eslint/visitor-keys": "6.21.0",
+        "@typescript-eslint/types": "7.5.0",
+        "@typescript-eslint/visitor-keys": "7.5.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -2305,7 +2449,7 @@
         "ts-api-utils": "^1.0.1"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -2317,42 +2461,57 @@
         }
       }
     },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/@typescript-eslint/utils": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
-      "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz",
+      "integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@types/json-schema": "^7.0.12",
         "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "6.21.0",
-        "@typescript-eslint/types": "6.21.0",
-        "@typescript-eslint/typescript-estree": "6.21.0",
+        "@typescript-eslint/scope-manager": "7.5.0",
+        "@typescript-eslint/types": "7.5.0",
+        "@typescript-eslint/typescript-estree": "7.5.0",
         "semver": "^7.5.4"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
       },
       "peerDependencies": {
-        "eslint": "^7.0.0 || ^8.0.0"
+        "eslint": "^8.56.0"
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "6.21.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
-      "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz",
+      "integrity": "sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/types": "7.5.0",
         "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
-        "node": "^16.0.0 || >=18.0.0"
+        "node": "^18.18.0 || >=20.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -2366,9 +2525,9 @@
       "dev": true
     },
     "node_modules/@vitejs/plugin-vue": {
-      "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz",
-      "integrity": "sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==",
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz",
+      "integrity": "sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==",
       "dev": true,
       "engines": {
         "node": "^18.0.0 || >=20.0.0"
@@ -2379,13 +2538,13 @@
       }
     },
     "node_modules/@vitest/expect": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz",
-      "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz",
+      "integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==",
       "dev": true,
       "dependencies": {
-        "@vitest/spy": "1.2.2",
-        "@vitest/utils": "1.2.2",
+        "@vitest/spy": "1.4.0",
+        "@vitest/utils": "1.4.0",
         "chai": "^4.3.10"
       },
       "funding": {
@@ -2393,12 +2552,12 @@
       }
     },
     "node_modules/@vitest/runner": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz",
-      "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz",
+      "integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==",
       "dev": true,
       "dependencies": {
-        "@vitest/utils": "1.2.2",
+        "@vitest/utils": "1.4.0",
         "p-limit": "^5.0.0",
         "pathe": "^1.1.1"
       },
@@ -2434,9 +2593,9 @@
       }
     },
     "node_modules/@vitest/snapshot": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz",
-      "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz",
+      "integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==",
       "dev": true,
       "dependencies": {
         "magic-string": "^0.30.5",
@@ -2448,9 +2607,9 @@
       }
     },
     "node_modules/@vitest/snapshot/node_modules/magic-string": {
-      "version": "0.30.7",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
-      "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
+      "version": "0.30.9",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz",
+      "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==",
       "dev": true,
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
@@ -2460,9 +2619,9 @@
       }
     },
     "node_modules/@vitest/spy": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz",
-      "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz",
+      "integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==",
       "dev": true,
       "dependencies": {
         "tinyspy": "^2.2.0"
@@ -2472,9 +2631,9 @@
       }
     },
     "node_modules/@vitest/utils": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz",
-      "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz",
+      "integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==",
       "dev": true,
       "dependencies": {
         "diff-sequences": "^29.6.3",
@@ -2502,46 +2661,46 @@
       }
     },
     "node_modules/@vue/compiler-core": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz",
-      "integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz",
+      "integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==",
       "dependencies": {
-        "@babel/parser": "^7.23.6",
-        "@vue/shared": "3.4.15",
+        "@babel/parser": "^7.23.9",
+        "@vue/shared": "3.4.21",
         "entities": "^4.5.0",
         "estree-walker": "^2.0.2",
         "source-map-js": "^1.0.2"
       }
     },
     "node_modules/@vue/compiler-dom": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz",
-      "integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz",
+      "integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==",
       "dependencies": {
-        "@vue/compiler-core": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/compiler-core": "3.4.21",
+        "@vue/shared": "3.4.21"
       }
     },
     "node_modules/@vue/compiler-sfc": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz",
-      "integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz",
+      "integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==",
       "dependencies": {
-        "@babel/parser": "^7.23.6",
-        "@vue/compiler-core": "3.4.15",
-        "@vue/compiler-dom": "3.4.15",
-        "@vue/compiler-ssr": "3.4.15",
-        "@vue/shared": "3.4.15",
+        "@babel/parser": "^7.23.9",
+        "@vue/compiler-core": "3.4.21",
+        "@vue/compiler-dom": "3.4.21",
+        "@vue/compiler-ssr": "3.4.21",
+        "@vue/shared": "3.4.21",
         "estree-walker": "^2.0.2",
-        "magic-string": "^0.30.5",
-        "postcss": "^8.4.33",
+        "magic-string": "^0.30.7",
+        "postcss": "^8.4.35",
         "source-map-js": "^1.0.2"
       }
     },
     "node_modules/@vue/compiler-sfc/node_modules/magic-string": {
-      "version": "0.30.6",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.6.tgz",
-      "integrity": "sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==",
+      "version": "0.30.9",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz",
+      "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==",
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
       },
@@ -2550,62 +2709,62 @@
       }
     },
     "node_modules/@vue/compiler-ssr": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz",
-      "integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz",
+      "integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==",
       "dependencies": {
-        "@vue/compiler-dom": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/compiler-dom": "3.4.21",
+        "@vue/shared": "3.4.21"
       }
     },
     "node_modules/@vue/reactivity": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz",
-      "integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz",
+      "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==",
       "dependencies": {
-        "@vue/shared": "3.4.15"
+        "@vue/shared": "3.4.21"
       }
     },
     "node_modules/@vue/runtime-core": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz",
-      "integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz",
+      "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==",
       "dependencies": {
-        "@vue/reactivity": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/reactivity": "3.4.21",
+        "@vue/shared": "3.4.21"
       }
     },
     "node_modules/@vue/runtime-dom": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz",
-      "integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz",
+      "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==",
       "dependencies": {
-        "@vue/runtime-core": "3.4.15",
-        "@vue/shared": "3.4.15",
+        "@vue/runtime-core": "3.4.21",
+        "@vue/shared": "3.4.21",
         "csstype": "^3.1.3"
       }
     },
     "node_modules/@vue/server-renderer": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz",
-      "integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz",
+      "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==",
       "dependencies": {
-        "@vue/compiler-ssr": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/compiler-ssr": "3.4.21",
+        "@vue/shared": "3.4.21"
       },
       "peerDependencies": {
-        "vue": "3.4.15"
+        "vue": "3.4.21"
       }
     },
     "node_modules/@vue/shared": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
-      "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g=="
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
+      "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g=="
     },
     "node_modules/@webassemblyjs/ast": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
-      "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
+      "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==",
       "dependencies": {
         "@webassemblyjs/helper-numbers": "1.11.6",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
@@ -2622,9 +2781,9 @@
       "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
     },
     "node_modules/@webassemblyjs/helper-buffer": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
-      "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA=="
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz",
+      "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw=="
     },
     "node_modules/@webassemblyjs/helper-numbers": {
       "version": "1.11.6",
@@ -2642,14 +2801,14 @@
       "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
     },
     "node_modules/@webassemblyjs/helper-wasm-section": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
-      "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz",
+      "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==",
       "dependencies": {
-        "@webassemblyjs/ast": "1.11.6",
-        "@webassemblyjs/helper-buffer": "1.11.6",
+        "@webassemblyjs/ast": "1.12.1",
+        "@webassemblyjs/helper-buffer": "1.12.1",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-        "@webassemblyjs/wasm-gen": "1.11.6"
+        "@webassemblyjs/wasm-gen": "1.12.1"
       }
     },
     "node_modules/@webassemblyjs/ieee754": {
@@ -2674,26 +2833,26 @@
       "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
     },
     "node_modules/@webassemblyjs/wasm-edit": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
-      "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz",
+      "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==",
       "dependencies": {
-        "@webassemblyjs/ast": "1.11.6",
-        "@webassemblyjs/helper-buffer": "1.11.6",
+        "@webassemblyjs/ast": "1.12.1",
+        "@webassemblyjs/helper-buffer": "1.12.1",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-        "@webassemblyjs/helper-wasm-section": "1.11.6",
-        "@webassemblyjs/wasm-gen": "1.11.6",
-        "@webassemblyjs/wasm-opt": "1.11.6",
-        "@webassemblyjs/wasm-parser": "1.11.6",
-        "@webassemblyjs/wast-printer": "1.11.6"
+        "@webassemblyjs/helper-wasm-section": "1.12.1",
+        "@webassemblyjs/wasm-gen": "1.12.1",
+        "@webassemblyjs/wasm-opt": "1.12.1",
+        "@webassemblyjs/wasm-parser": "1.12.1",
+        "@webassemblyjs/wast-printer": "1.12.1"
       }
     },
     "node_modules/@webassemblyjs/wasm-gen": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
-      "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz",
+      "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==",
       "dependencies": {
-        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/ast": "1.12.1",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
         "@webassemblyjs/ieee754": "1.11.6",
         "@webassemblyjs/leb128": "1.11.6",
@@ -2701,22 +2860,22 @@
       }
     },
     "node_modules/@webassemblyjs/wasm-opt": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
-      "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz",
+      "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==",
       "dependencies": {
-        "@webassemblyjs/ast": "1.11.6",
-        "@webassemblyjs/helper-buffer": "1.11.6",
-        "@webassemblyjs/wasm-gen": "1.11.6",
-        "@webassemblyjs/wasm-parser": "1.11.6"
+        "@webassemblyjs/ast": "1.12.1",
+        "@webassemblyjs/helper-buffer": "1.12.1",
+        "@webassemblyjs/wasm-gen": "1.12.1",
+        "@webassemblyjs/wasm-parser": "1.12.1"
       }
     },
     "node_modules/@webassemblyjs/wasm-parser": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
-      "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz",
+      "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==",
       "dependencies": {
-        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/ast": "1.12.1",
         "@webassemblyjs/helper-api-error": "1.11.6",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
         "@webassemblyjs/ieee754": "1.11.6",
@@ -2725,19 +2884,14 @@
       }
     },
     "node_modules/@webassemblyjs/wast-printer": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
-      "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz",
+      "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==",
       "dependencies": {
-        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/ast": "1.12.1",
         "@xtuc/long": "4.2.2"
       }
     },
-    "node_modules/@webcomponents/custom-elements": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.6.0.tgz",
-      "integrity": "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww=="
-    },
     "node_modules/@webpack-cli/configtest": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
@@ -2852,18 +3006,6 @@
         "webpack": ">=5"
       }
     },
-    "node_modules/agent-base": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
-      "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
-      "dev": true,
-      "dependencies": {
-        "debug": "^4.3.4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
     "node_modules/ajv": {
       "version": "8.12.0",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
@@ -2959,19 +3101,53 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/arg": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
+    },
     "node_modules/argparse": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
     },
-    "node_modules/array-buffer-byte-length": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
-      "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+    "node_modules/aria-query": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+      "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "is-array-buffer": "^3.0.1"
+        "dequal": "^2.0.3"
+      }
+    },
+    "node_modules/array-buffer-byte-length": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
+      "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.5",
+        "is-array-buffer": "^3.0.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -2985,6 +3161,26 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/array-includes": {
+      "version": "3.1.8",
+      "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
+      "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "is-string": "^1.0.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/array-union": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -2994,18 +3190,75 @@
         "node": ">=8"
       }
     },
-    "node_modules/arraybuffer.prototype.slice": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz",
-      "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==",
+    "node_modules/array.prototype.findlastindex": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz",
+      "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.flat": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
+      "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
       "dev": true,
       "dependencies": {
-        "array-buffer-byte-length": "^1.0.0",
         "call-bind": "^1.0.2",
         "define-properties": "^1.2.0",
         "es-abstract": "^1.22.1",
-        "get-intrinsic": "^1.2.1",
-        "is-array-buffer": "^3.0.2",
+        "es-shim-unscopables": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.flatmap": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
+      "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1",
+        "es-shim-unscopables": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/arraybuffer.prototype.slice": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
+      "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==",
+      "dev": true,
+      "dependencies": {
+        "array-buffer-byte-length": "^1.0.1",
+        "call-bind": "^1.0.5",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.22.3",
+        "es-errors": "^1.2.1",
+        "get-intrinsic": "^1.2.3",
+        "is-array-buffer": "^3.0.4",
         "is-shared-array-buffer": "^1.0.2"
       },
       "engines": {
@@ -3025,9 +3278,9 @@
       }
     },
     "node_modules/asciinema-player": {
-      "version": "3.6.3",
-      "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.6.3.tgz",
-      "integrity": "sha512-62aDgLpbuduhmpFfNgPOzf6fOluACLsftVnjpWJjUXX6dqhqTckFqWoJ+ayA0XjSlZ9l9wXTcJqRqvAAIpMblg==",
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.7.1.tgz",
+      "integrity": "sha512-zDJteGjBzNQhHEnD0aG7GqV3E53sOyKb1WCxKNRm2PquU70Lq3s4xxb91wyDS0hBJ3J/TB8aY3y8gjGPN+T23A==",
       "dependencies": {
         "@babel/runtime": "^7.21.0",
         "solid-js": "^1.3.0"
@@ -3054,6 +3307,12 @@
         "node": ">=4"
       }
     },
+    "node_modules/ast-types-flow": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+      "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+      "dev": true
+    },
     "node_modules/astral-regex": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
@@ -3072,12 +3331,6 @@
         "astring": "bin/astring"
       }
     },
-    "node_modules/asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
-      "dev": true
-    },
     "node_modules/atob": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -3091,10 +3344,13 @@
       }
     },
     "node_modules/available-typed-arrays": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
-      "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+      "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
       "dev": true,
+      "dependencies": {
+        "possible-typed-array-names": "^1.0.0"
+      },
       "engines": {
         "node": ">= 0.4"
       },
@@ -3102,6 +3358,24 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/axe-core": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz",
+      "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/axobject-query": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
+      "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==",
+      "dev": true,
+      "dependencies": {
+        "dequal": "^2.0.3"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3134,6 +3408,17 @@
         "node": "*"
       }
     },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/boolbase": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -3160,9 +3445,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.22.3",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
-      "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
+      "version": "4.23.0",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
+      "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -3178,8 +3463,8 @@
         }
       ],
       "dependencies": {
-        "caniuse-lite": "^1.0.30001580",
-        "electron-to-chromium": "^1.4.648",
+        "caniuse-lite": "^1.0.30001587",
+        "electron-to-chromium": "^1.4.668",
         "node-releases": "^2.0.14",
         "update-browserslist-db": "^1.0.13"
       },
@@ -3246,14 +3531,19 @@
       }
     },
     "node_modules/call-bind": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
-      "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+      "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
       "dev": true,
       "dependencies": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.1",
-        "set-function-length": "^1.1.1"
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -3263,15 +3553,22 @@
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
       "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
-      "dev": true,
       "engines": {
         "node": ">=6"
       }
     },
+    "node_modules/camelcase-css": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001581",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz",
-      "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==",
+      "version": "1.0.30001605",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001605.tgz",
+      "integrity": "sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -3329,6 +3626,40 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/chart.js": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz",
+      "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==",
+      "dependencies": {
+        "@kurkle/color": "^0.3.0"
+      },
+      "engines": {
+        "pnpm": ">=8"
+      }
+    },
+    "node_modules/chartjs-adapter-dayjs-4": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/chartjs-adapter-dayjs-4/-/chartjs-adapter-dayjs-4-1.0.4.tgz",
+      "integrity": "sha512-yy9BAYW4aNzPVrCWZetbILegTRb7HokhgospPoC3b5iZ5qdlqNmXts2KdSp6AqnjkPAp/YWyHDxLvIvwt5x81w==",
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "chart.js": ">=4.0.1",
+        "dayjs": "^1.9.7"
+      }
+    },
+    "node_modules/chartjs-plugin-zoom": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.0.1.tgz",
+      "integrity": "sha512-ogOmLu6e+Q7E1XWOCOz9YwybMslz9qNfGV2a+qjfmqJYpsw5ZMoRHZBUyW+NGhkpQ5PwwPA/+rikHpBZb7PZuA==",
+      "dependencies": {
+        "hammerjs": "^2.0.8"
+      },
+      "peerDependencies": {
+        "chart.js": ">=3.2.0"
+      }
+    },
     "node_modules/check-error": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
@@ -3341,6 +3672,40 @@
         "node": "*"
       }
     },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/chrome-trace-event": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
@@ -3391,9 +3756,9 @@
       }
     },
     "node_modules/clippie": {
-      "version": "4.0.6",
-      "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.0.6.tgz",
-      "integrity": "sha512-E5EtOw8iMH0enuL3kBZJ+Po1nPnBD7O+HHpIaWpfWgHbHmdoOQoERrlNOcEEn2yMJQ98WqeKacouAcnRXn7oWA=="
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.0.7.tgz",
+      "integrity": "sha512-xmIARCRFQUoCR0kNNu4uIv5f/IFqM1fUts0vQwt1hQEdCPEqs3/dTaG38WenlWOgs3Fcn73PBYXbPIVSlOgFRw=="
     },
     "node_modules/cliui": {
       "version": "7.0.4",
@@ -3487,18 +3852,6 @@
       "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
       "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="
     },
-    "node_modules/combined-stream": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
-      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "dev": true,
-      "dependencies": {
-        "delayed-stream": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
     "node_modules/commander": {
       "version": "8.3.0",
       "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
@@ -3528,12 +3881,12 @@
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
     },
     "node_modules/core-js-compat": {
-      "version": "3.35.1",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
-      "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==",
+      "version": "3.36.1",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz",
+      "integrity": "sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==",
       "dev": true,
       "dependencies": {
-        "browserslist": "^4.22.2"
+        "browserslist": "^4.23.0"
       },
       "funding": {
         "type": "opencollective",
@@ -3552,7 +3905,6 @@
       "version": "9.0.0",
       "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
       "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
-      "dev": true,
       "dependencies": {
         "env-paths": "^2.2.1",
         "import-fresh": "^3.3.0",
@@ -3608,21 +3960,21 @@
       }
     },
     "node_modules/css-loader": {
-      "version": "6.10.0",
-      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz",
-      "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.0.0.tgz",
+      "integrity": "sha512-WrO4FVoamxt5zY9CauZjoJgXRi/LZKIk+Ta7YvpSGr5r/eMYPNp5/T9ODlMe4/1rF5DYlycG1avhV4g3A/tiAw==",
       "dependencies": {
         "icss-utils": "^5.1.0",
         "postcss": "^8.4.33",
-        "postcss-modules-extract-imports": "^3.0.0",
-        "postcss-modules-local-by-default": "^4.0.4",
-        "postcss-modules-scope": "^3.1.1",
+        "postcss-modules-extract-imports": "^3.1.0",
+        "postcss-modules-local-by-default": "^4.0.5",
+        "postcss-modules-scope": "^3.2.0",
         "postcss-modules-values": "^4.0.0",
         "postcss-value-parser": "^4.2.0",
         "semver": "^7.5.4"
       },
       "engines": {
-        "node": ">= 12.13.0"
+        "node": ">= 18.12.0"
       },
       "funding": {
         "type": "opencollective",
@@ -3630,7 +3982,7 @@
       },
       "peerDependencies": {
         "@rspack/core": "0.x || 1.x",
-        "webpack": "^5.0.0"
+        "webpack": "^5.27.0"
       },
       "peerDependenciesMeta": {
         "@rspack/core": {
@@ -3726,18 +4078,6 @@
       "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
       "dev": true
     },
-    "node_modules/cssstyle": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz",
-      "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==",
-      "dev": true,
-      "dependencies": {
-        "rrweb-cssom": "^0.6.0"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
     "node_modules/csstype": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -3766,34 +4106,10 @@
         "cytoscape": "^3.2.0"
       }
     },
-    "node_modules/cytoscape-fcose": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
-      "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
-      "dependencies": {
-        "cose-base": "^2.2.0"
-      },
-      "peerDependencies": {
-        "cytoscape": "^3.2.0"
-      }
-    },
-    "node_modules/cytoscape-fcose/node_modules/cose-base": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
-      "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
-      "dependencies": {
-        "layout-base": "^2.0.0"
-      }
-    },
-    "node_modules/cytoscape-fcose/node_modules/layout-base": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
-      "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="
-    },
     "node_modules/d3": {
-      "version": "7.8.5",
-      "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz",
-      "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+      "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
       "dependencies": {
         "d3-array": "3",
         "d3-axis": "3",
@@ -3998,9 +4314,9 @@
       }
     },
     "node_modules/d3-geo": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz",
-      "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+      "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
       "dependencies": {
         "d3-array": "2.5.0 - 3"
       },
@@ -4110,9 +4426,9 @@
       }
     },
     "node_modules/d3-scale-chromatic": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz",
-      "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+      "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
       "dependencies": {
         "d3-color": "1 - 3",
         "d3-interpolate": "1 - 3"
@@ -4212,23 +4528,67 @@
         "lodash-es": "^4.17.21"
       }
     },
+    "node_modules/damerau-levenshtein": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+      "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+      "dev": true
+    },
     "node_modules/data-uri-to-buffer": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz",
       "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==",
       "dev": true
     },
-    "node_modules/data-urls": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
-      "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+    "node_modules/data-view-buffer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
+      "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==",
       "dev": true,
       "dependencies": {
-        "whatwg-mimetype": "^4.0.0",
-        "whatwg-url": "^14.0.0"
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
       },
       "engines": {
-        "node": ">=18"
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/data-view-byte-length": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz",
+      "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/data-view-byte-offset": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz",
+      "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/dayjs": {
@@ -4252,12 +4612,6 @@
         }
       }
     },
-    "node_modules/decimal.js": {
-      "version": "10.4.3",
-      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
-      "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
-      "dev": true
-    },
     "node_modules/decode-named-character-reference": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
@@ -4307,17 +4661,20 @@
       "dev": true
     },
     "node_modules/define-data-property": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
-      "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
       "dev": true,
       "dependencies": {
-        "get-intrinsic": "^1.2.1",
-        "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0"
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
       },
       "engines": {
         "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/define-properties": {
@@ -4345,15 +4702,6 @@
         "robust-predicates": "^3.0.2"
       }
     },
-    "node_modules/delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
     "node_modules/dependency-graph": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz",
@@ -4371,10 +4719,15 @@
         "node": ">=6"
       }
     },
+    "node_modules/didyoumean": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
+    },
     "node_modules/diff": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
-      "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+      "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
       "engines": {
         "node": ">=0.3.1"
       }
@@ -4400,6 +4753,11 @@
         "node": ">=8"
       }
     },
+    "node_modules/dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
+    },
     "node_modules/doctrine": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -4454,9 +4812,9 @@
       }
     },
     "node_modules/dompurify": {
-      "version": "3.0.8",
-      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.8.tgz",
-      "integrity": "sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ=="
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.11.tgz",
+      "integrity": "sha512-Fan4uMuyB26gFV3ovPoEoQbxRRPfTu3CvImyZnhGq5fsIEO+gEFLp45ISFt+kQBWsK5ulDdT0oV28jS1UrwQLg=="
     },
     "node_modules/domutils": {
       "version": "3.1.0",
@@ -4484,8 +4842,7 @@
     "node_modules/eastasianwidth": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
-      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
-      "dev": true
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
     },
     "node_modules/easymde": {
       "version": "2.18.0",
@@ -4500,19 +4857,19 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.653",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.653.tgz",
-      "integrity": "sha512-wA2A2LQCqnEwQAvwADQq3KpMpNwgAUBnRmrFgRzHnPhbQUFArTR32Ab46f4p0MovDLcg4uqd4nCsN2hTltslpA=="
+      "version": "1.4.727",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.727.tgz",
+      "integrity": "sha512-brpv4KTeC4g0Fx2FeIKytLd4UGn1zBQq5Lauy7zEWT9oqkaj5mgsxblEZIAOf1HHLlXxzr6adGViiBy5Z39/CA=="
     },
     "node_modules/elkjs": {
-      "version": "0.9.1",
-      "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.1.tgz",
-      "integrity": "sha512-JWKDyqAdltuUcyxaECtYG6H4sqysXSLeoXuGUBfRNESMTkj+w+qdb0jya8Z/WI0jVd03WQtCGhS6FOFtlhD5FQ=="
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.2.tgz",
+      "integrity": "sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw=="
     },
     "node_modules/emoji-regex": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
     },
     "node_modules/emojis-list": {
       "version": "3.0.0",
@@ -4523,9 +4880,9 @@
       }
     },
     "node_modules/enhanced-resolve": {
-      "version": "5.15.0",
-      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
-      "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
+      "version": "5.16.0",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz",
+      "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==",
       "dependencies": {
         "graceful-fs": "^4.2.4",
         "tapable": "^2.2.0"
@@ -4549,15 +4906,14 @@
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
       "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
-      "dev": true,
       "engines": {
         "node": ">=6"
       }
     },
     "node_modules/envinfo": {
-      "version": "7.11.0",
-      "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz",
-      "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==",
+      "version": "7.11.1",
+      "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz",
+      "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==",
       "bin": {
         "envinfo": "dist/cli.js"
       },
@@ -4569,56 +4925,62 @@
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
       "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
-      "dev": true,
       "dependencies": {
         "is-arrayish": "^0.2.1"
       }
     },
     "node_modules/es-abstract": {
-      "version": "1.22.3",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz",
-      "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==",
+      "version": "1.23.3",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
+      "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==",
       "dev": true,
       "dependencies": {
-        "array-buffer-byte-length": "^1.0.0",
-        "arraybuffer.prototype.slice": "^1.0.2",
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.5",
-        "es-set-tostringtag": "^2.0.1",
+        "array-buffer-byte-length": "^1.0.1",
+        "arraybuffer.prototype.slice": "^1.0.3",
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.7",
+        "data-view-buffer": "^1.0.1",
+        "data-view-byte-length": "^1.0.1",
+        "data-view-byte-offset": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "es-set-tostringtag": "^2.0.3",
         "es-to-primitive": "^1.2.1",
         "function.prototype.name": "^1.1.6",
-        "get-intrinsic": "^1.2.2",
-        "get-symbol-description": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "get-symbol-description": "^1.0.2",
         "globalthis": "^1.0.3",
         "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0",
-        "has-proto": "^1.0.1",
+        "has-property-descriptors": "^1.0.2",
+        "has-proto": "^1.0.3",
         "has-symbols": "^1.0.3",
-        "hasown": "^2.0.0",
-        "internal-slot": "^1.0.5",
-        "is-array-buffer": "^3.0.2",
+        "hasown": "^2.0.2",
+        "internal-slot": "^1.0.7",
+        "is-array-buffer": "^3.0.4",
         "is-callable": "^1.2.7",
-        "is-negative-zero": "^2.0.2",
+        "is-data-view": "^1.0.1",
+        "is-negative-zero": "^2.0.3",
         "is-regex": "^1.1.4",
-        "is-shared-array-buffer": "^1.0.2",
+        "is-shared-array-buffer": "^1.0.3",
         "is-string": "^1.0.7",
-        "is-typed-array": "^1.1.12",
+        "is-typed-array": "^1.1.13",
         "is-weakref": "^1.0.2",
         "object-inspect": "^1.13.1",
         "object-keys": "^1.1.1",
-        "object.assign": "^4.1.4",
-        "regexp.prototype.flags": "^1.5.1",
-        "safe-array-concat": "^1.0.1",
-        "safe-regex-test": "^1.0.0",
-        "string.prototype.trim": "^1.2.8",
-        "string.prototype.trimend": "^1.0.7",
-        "string.prototype.trimstart": "^1.0.7",
-        "typed-array-buffer": "^1.0.0",
-        "typed-array-byte-length": "^1.0.0",
-        "typed-array-byte-offset": "^1.0.0",
-        "typed-array-length": "^1.0.4",
+        "object.assign": "^4.1.5",
+        "regexp.prototype.flags": "^1.5.2",
+        "safe-array-concat": "^1.1.2",
+        "safe-regex-test": "^1.0.3",
+        "string.prototype.trim": "^1.2.9",
+        "string.prototype.trimend": "^1.0.8",
+        "string.prototype.trimstart": "^1.0.8",
+        "typed-array-buffer": "^1.0.2",
+        "typed-array-byte-length": "^1.0.1",
+        "typed-array-byte-offset": "^1.0.2",
+        "typed-array-length": "^1.0.6",
         "unbox-primitive": "^1.0.2",
-        "which-typed-array": "^1.1.13"
+        "which-typed-array": "^1.1.15"
       },
       "engines": {
         "node": ">= 0.4"
@@ -4628,19 +4990,19 @@
       }
     },
     "node_modules/es-aggregate-error": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.11.tgz",
-      "integrity": "sha512-DCiZiNlMlbvofET/cE55My387NiLvuGToBEZDdK9U2G3svDCjL8WOgO5Il6lO83nQ8qmag/R9nArdpaFQ/m3lA==",
+      "version": "1.0.13",
+      "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.13.tgz",
+      "integrity": "sha512-KkzhUUuD2CUMqEc8JEqsXEMDHzDPE8RCjZeUBitsnB1eNcAJWQPiciKsMXe3Yytj4Flw1XLl46Qcf9OxvZha7A==",
       "dev": true,
       "dependencies": {
-        "define-data-property": "^1.1.0",
+        "define-data-property": "^1.1.4",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.22.1",
-        "function-bind": "^1.1.1",
-        "get-intrinsic": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
         "globalthis": "^1.0.3",
-        "has-property-descriptors": "^1.0.0",
-        "set-function-name": "^2.0.1"
+        "has-property-descriptors": "^1.0.2",
+        "set-function-name": "^2.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -4649,25 +5011,92 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/es-module-lexer": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
-      "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="
-    },
-    "node_modules/es-set-tostringtag": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
-      "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
+    "node_modules/es-define-property": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+      "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
       "dev": true,
       "dependencies": {
-        "get-intrinsic": "^1.2.2",
-        "has-tostringtag": "^1.0.0",
-        "hasown": "^2.0.0"
+        "get-intrinsic": "^1.2.4"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-iterator-helpers": {
+      "version": "1.0.18",
+      "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz",
+      "integrity": "sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.0",
+        "es-errors": "^1.3.0",
+        "es-set-tostringtag": "^2.0.3",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "globalthis": "^1.0.3",
+        "has-property-descriptors": "^1.0.2",
+        "has-proto": "^1.0.3",
+        "has-symbols": "^1.0.3",
+        "internal-slot": "^1.0.7",
+        "iterator.prototype": "^1.1.2",
+        "safe-array-concat": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz",
+      "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw=="
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
+      "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
+      "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
+      "dev": true,
+      "dependencies": {
+        "get-intrinsic": "^1.2.4",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-shim-unscopables": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
+      "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
+      "dev": true,
+      "dependencies": {
+        "hasown": "^2.0.0"
+      }
+    },
     "node_modules/es-to-primitive": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
@@ -4686,9 +5115,9 @@
       }
     },
     "node_modules/esbuild": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
-      "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
+      "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
       "hasInstallScript": true,
       "bin": {
         "esbuild": "bin/esbuild"
@@ -4697,37 +5126,37 @@
         "node": ">=12"
       },
       "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.19.12",
-        "@esbuild/android-arm": "0.19.12",
-        "@esbuild/android-arm64": "0.19.12",
-        "@esbuild/android-x64": "0.19.12",
-        "@esbuild/darwin-arm64": "0.19.12",
-        "@esbuild/darwin-x64": "0.19.12",
-        "@esbuild/freebsd-arm64": "0.19.12",
-        "@esbuild/freebsd-x64": "0.19.12",
-        "@esbuild/linux-arm": "0.19.12",
-        "@esbuild/linux-arm64": "0.19.12",
-        "@esbuild/linux-ia32": "0.19.12",
-        "@esbuild/linux-loong64": "0.19.12",
-        "@esbuild/linux-mips64el": "0.19.12",
-        "@esbuild/linux-ppc64": "0.19.12",
-        "@esbuild/linux-riscv64": "0.19.12",
-        "@esbuild/linux-s390x": "0.19.12",
-        "@esbuild/linux-x64": "0.19.12",
-        "@esbuild/netbsd-x64": "0.19.12",
-        "@esbuild/openbsd-x64": "0.19.12",
-        "@esbuild/sunos-x64": "0.19.12",
-        "@esbuild/win32-arm64": "0.19.12",
-        "@esbuild/win32-ia32": "0.19.12",
-        "@esbuild/win32-x64": "0.19.12"
+        "@esbuild/aix-ppc64": "0.20.2",
+        "@esbuild/android-arm": "0.20.2",
+        "@esbuild/android-arm64": "0.20.2",
+        "@esbuild/android-x64": "0.20.2",
+        "@esbuild/darwin-arm64": "0.20.2",
+        "@esbuild/darwin-x64": "0.20.2",
+        "@esbuild/freebsd-arm64": "0.20.2",
+        "@esbuild/freebsd-x64": "0.20.2",
+        "@esbuild/linux-arm": "0.20.2",
+        "@esbuild/linux-arm64": "0.20.2",
+        "@esbuild/linux-ia32": "0.20.2",
+        "@esbuild/linux-loong64": "0.20.2",
+        "@esbuild/linux-mips64el": "0.20.2",
+        "@esbuild/linux-ppc64": "0.20.2",
+        "@esbuild/linux-riscv64": "0.20.2",
+        "@esbuild/linux-s390x": "0.20.2",
+        "@esbuild/linux-x64": "0.20.2",
+        "@esbuild/netbsd-x64": "0.20.2",
+        "@esbuild/openbsd-x64": "0.20.2",
+        "@esbuild/sunos-x64": "0.20.2",
+        "@esbuild/win32-arm64": "0.20.2",
+        "@esbuild/win32-ia32": "0.20.2",
+        "@esbuild/win32-x64": "0.20.2"
       }
     },
     "node_modules/esbuild-loader": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-4.0.3.tgz",
-      "integrity": "sha512-YpaSRisj7TSg6maKKKG9OJGGm0BZ7EXeov8J8cXEYdugjlAJ0wL7aj2JactoQvPJ113v2Ar204pdJWrZsAQc8Q==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-4.1.0.tgz",
+      "integrity": "sha512-543TtIvqbqouEMlOHg4xKoDQkmdImlwIpyAIgpUtDPvMuklU/c2k+Qt2O3VeDBgAwozxmlEbjOzV+F8CZ0g+Bw==",
       "dependencies": {
-        "esbuild": "^0.19.0",
+        "esbuild": "^0.20.0",
         "get-tsconfig": "^4.7.0",
         "loader-utils": "^2.0.4",
         "webpack-sources": "^1.4.3"
@@ -4740,9 +5169,9 @@
       }
     },
     "node_modules/escalade": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
       "engines": {
         "node": ">=6"
       }
@@ -4771,16 +5200,16 @@
       }
     },
     "node_modules/eslint": {
-      "version": "8.56.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
-      "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
+      "version": "8.57.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
+      "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.6.1",
         "@eslint/eslintrc": "^2.1.4",
-        "@eslint/js": "8.56.0",
-        "@humanwhocodes/config-array": "^0.11.13",
+        "@eslint/js": "8.57.0",
+        "@humanwhocodes/config-array": "^0.11.14",
         "@humanwhocodes/module-importer": "^1.0.1",
         "@nodelib/fs.walk": "^1.2.8",
         "@ungap/structured-clone": "^1.2.0",
@@ -4826,9 +5255,9 @@
       }
     },
     "node_modules/eslint-compat-utils": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.4.1.tgz",
-      "integrity": "sha512-5N7ZaJG5pZxUeNNJfUchurLVrunD1xJvyg5kYOIVF8kg1f3ajTikmAu/5fZ9w100omNPOoMjngRszh/Q/uFGMg==",
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.0.tgz",
+      "integrity": "sha512-dc6Y8tzEcSYZMHa+CMPLi/hyo1FzNeonbhJL7Ol0ccuKQkwopJcJBA9YL/xmMTLU1eKigXo9vj9nALElWYSowg==",
       "dev": true,
       "dependencies": {
         "semver": "^7.5.4"
@@ -4840,6 +5269,18 @@
         "eslint": ">=6.0.0"
       }
     },
+    "node_modules/eslint-config-prettier": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+      "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+      "dev": true,
+      "bin": {
+        "eslint-config-prettier": "bin/cli.js"
+      },
+      "peerDependencies": {
+        "eslint": ">=7.0.0"
+      }
+    },
     "node_modules/eslint-import-resolver-node": {
       "version": "0.3.9",
       "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
@@ -4861,9 +5302,9 @@
       }
     },
     "node_modules/eslint-module-utils": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
-      "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz",
+      "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==",
       "dev": true,
       "dependencies": {
         "debug": "^3.2.7"
@@ -4898,6 +5339,92 @@
         "eslint": ">=8.40.0"
       }
     },
+    "node_modules/eslint-plugin-escompat": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-escompat/-/eslint-plugin-escompat-3.4.0.tgz",
+      "integrity": "sha512-ufTPv8cwCxTNoLnTZBFTQ5SxU2w7E7wiMIS7PSxsgP1eAxFjtSaoZ80LRn64hI8iYziE6kJG6gX/ZCJVxh48Bg==",
+      "dev": true,
+      "dependencies": {
+        "browserslist": "^4.21.0"
+      },
+      "peerDependencies": {
+        "eslint": ">=5.14.1"
+      }
+    },
+    "node_modules/eslint-plugin-eslint-comments": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz",
+      "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==",
+      "dev": true,
+      "dependencies": {
+        "escape-string-regexp": "^1.0.5",
+        "ignore": "^5.0.5"
+      },
+      "engines": {
+        "node": ">=6.5.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mysticatea"
+      },
+      "peerDependencies": {
+        "eslint": ">=4.19.1"
+      }
+    },
+    "node_modules/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/eslint-plugin-filenames": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-filenames/-/eslint-plugin-filenames-1.3.2.tgz",
+      "integrity": "sha512-tqxJTiEM5a0JmRCUYQmxw23vtTxrb2+a3Q2mMOPhFxvt7ZQQJmdiuMby9B/vUAuVMghyP7oET+nIf6EO6CBd/w==",
+      "dev": true,
+      "dependencies": {
+        "lodash.camelcase": "4.3.0",
+        "lodash.kebabcase": "4.1.1",
+        "lodash.snakecase": "4.1.1",
+        "lodash.upperfirst": "4.3.1"
+      },
+      "peerDependencies": {
+        "eslint": "*"
+      }
+    },
+    "node_modules/eslint-plugin-github": {
+      "version": "4.10.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-4.10.2.tgz",
+      "integrity": "sha512-F1F5aAFgi1Y5hYoTFzGQACBkw5W1hu2Fu5FSTrMlXqrojJnKl1S2pWO/rprlowRQpt+hzHhqSpsfnodJEVd5QA==",
+      "dev": true,
+      "dependencies": {
+        "@github/browserslist-config": "^1.0.0",
+        "@typescript-eslint/eslint-plugin": "^7.0.1",
+        "@typescript-eslint/parser": "^7.0.1",
+        "aria-query": "^5.3.0",
+        "eslint-config-prettier": ">=8.0.0",
+        "eslint-plugin-escompat": "^3.3.3",
+        "eslint-plugin-eslint-comments": "^3.2.0",
+        "eslint-plugin-filenames": "^1.3.2",
+        "eslint-plugin-i18n-text": "^1.0.1",
+        "eslint-plugin-import": "^2.25.2",
+        "eslint-plugin-jsx-a11y": "^6.7.1",
+        "eslint-plugin-no-only-tests": "^3.0.0",
+        "eslint-plugin-prettier": "^5.0.0",
+        "eslint-rule-documentation": ">=1.0.0",
+        "jsx-ast-utils": "^3.3.2",
+        "prettier": "^3.0.0",
+        "svg-element-attributes": "^1.3.1"
+      },
+      "bin": {
+        "eslint-ignore-errors": "bin/eslint-ignore-errors.js"
+      },
+      "peerDependencies": {
+        "eslint": "^8.0.1"
+      }
+    },
     "node_modules/eslint-plugin-i": {
       "version": "2.29.1",
       "resolved": "https://registry.npmjs.org/eslint-plugin-i/-/eslint-plugin-i-2.29.1.tgz",
@@ -4945,6 +5472,98 @@
         "node": "*"
       }
     },
+    "node_modules/eslint-plugin-i18n-text": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz",
+      "integrity": "sha512-3G3UetST6rdqhqW9SfcfzNYMpQXS7wNkJvp6dsXnjzGiku6Iu5hl3B0kmk6lIcFPwYjhQIY+tXVRtK9TlGT7RA==",
+      "dev": true,
+      "peerDependencies": {
+        "eslint": ">=5.0.0"
+      }
+    },
+    "node_modules/eslint-plugin-import": {
+      "version": "2.29.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
+      "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
+      "dev": true,
+      "dependencies": {
+        "array-includes": "^3.1.7",
+        "array.prototype.findlastindex": "^1.2.3",
+        "array.prototype.flat": "^1.3.2",
+        "array.prototype.flatmap": "^1.3.2",
+        "debug": "^3.2.7",
+        "doctrine": "^2.1.0",
+        "eslint-import-resolver-node": "^0.3.9",
+        "eslint-module-utils": "^2.8.0",
+        "hasown": "^2.0.0",
+        "is-core-module": "^2.13.1",
+        "is-glob": "^4.0.3",
+        "minimatch": "^3.1.2",
+        "object.fromentries": "^2.0.7",
+        "object.groupby": "^1.0.1",
+        "object.values": "^1.1.7",
+        "semver": "^6.3.1",
+        "tsconfig-paths": "^3.15.0"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependencies": {
+        "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dev": true,
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/doctrine": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+      "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+      "dev": true,
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/eslint-plugin-jquery": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/eslint-plugin-jquery/-/eslint-plugin-jquery-1.5.1.tgz",
@@ -4954,6 +5573,58 @@
         "eslint": ">=5.4.0"
       }
     },
+    "node_modules/eslint-plugin-jsx-a11y": {
+      "version": "6.8.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz",
+      "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.23.2",
+        "aria-query": "^5.3.0",
+        "array-includes": "^3.1.7",
+        "array.prototype.flatmap": "^1.3.2",
+        "ast-types-flow": "^0.0.8",
+        "axe-core": "=4.7.0",
+        "axobject-query": "^3.2.1",
+        "damerau-levenshtein": "^1.0.8",
+        "emoji-regex": "^9.2.2",
+        "es-iterator-helpers": "^1.0.15",
+        "hasown": "^2.0.0",
+        "jsx-ast-utils": "^3.3.5",
+        "language-tags": "^1.0.9",
+        "minimatch": "^3.1.2",
+        "object.entries": "^1.1.7",
+        "object.fromentries": "^2.0.7"
+      },
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependencies": {
+        "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+      }
+    },
+    "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/eslint-plugin-no-jquery": {
       "version": "2.7.0",
       "resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.7.0.tgz",
@@ -4963,6 +5634,15 @@
         "eslint": ">=2.3.0"
       }
     },
+    "node_modules/eslint-plugin-no-only-tests": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.1.0.tgz",
+      "integrity": "sha512-Lf4YW/bL6Un1R6A76pRZyE1dl1vr31G/ev8UzIc/geCgFWyrKil8hVjYqWVKGB/UIGmb6Slzs9T0wNezdSVegw==",
+      "dev": true,
+      "engines": {
+        "node": ">=5.0.0"
+      }
+    },
     "node_modules/eslint-plugin-no-use-extend-native": {
       "version": "0.5.0",
       "resolved": "https://registry.npmjs.org/eslint-plugin-no-use-extend-native/-/eslint-plugin-no-use-extend-native-0.5.0.tgz",
@@ -4978,10 +5658,40 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/eslint-plugin-prettier": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
+      "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
+      "dev": true,
+      "dependencies": {
+        "prettier-linter-helpers": "^1.0.0",
+        "synckit": "^0.8.6"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint-plugin-prettier"
+      },
+      "peerDependencies": {
+        "@types/eslint": ">=8.0.0",
+        "eslint": ">=8.0.0",
+        "eslint-config-prettier": "*",
+        "prettier": ">=3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/eslint": {
+          "optional": true
+        },
+        "eslint-config-prettier": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/eslint-plugin-regexp": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.2.0.tgz",
-      "integrity": "sha512-0kwpiWiLRVBkVr3oIRQLl196sXP/NF6DQFefv9jtR4ZOgQR+6WID2pIZ0I+wIt54qgBPwBB7Gm2a+ueh8/WsFQ==",
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.4.0.tgz",
+      "integrity": "sha512-OL2S6VPjQhs9s/NclQ0qattVq1J0GU8ox70/HIVy5Dxw+qbbdd7KQkyucsez2clEQjvdtDe12DTnPphFFUyXFg==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
@@ -5000,21 +5710,21 @@
       }
     },
     "node_modules/eslint-plugin-sonarjs": {
-      "version": "0.23.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.23.0.tgz",
-      "integrity": "sha512-z44T3PBf9W7qQ/aR+NmofOTyg6HLhSEZOPD4zhStqBpLoMp8GYhFksuUBnCxbnf1nfISpKBVkQhiBLFI/F4Wlg==",
+      "version": "0.25.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.25.1.tgz",
+      "integrity": "sha512-5IOKvj/GMBNqjxBdItfotfRHo7w48496GOu1hxdeXuD0mB1JBlDCViiLHETDTfA8pDAVSBimBEQoetRXYceQEw==",
       "dev": true,
       "engines": {
-        "node": ">=14"
+        "node": ">=16"
       },
       "peerDependencies": {
         "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
       }
     },
     "node_modules/eslint-plugin-unicorn": {
-      "version": "50.0.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz",
-      "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==",
+      "version": "52.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz",
+      "integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==",
       "dev": true,
       "dependencies": {
         "@babel/helper-validator-identifier": "^7.22.20",
@@ -5045,12 +5755,12 @@
       }
     },
     "node_modules/eslint-plugin-vitest": {
-      "version": "0.3.21",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.3.21.tgz",
-      "integrity": "sha512-oYwR1MrwaBw/OG6CKU+SJYleAc442w6CWL1RTQl5WLwy8X3sh0bgHIQk5iEtmTak3Q+XAvZglr0bIoDOjFdkcw==",
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.4.1.tgz",
+      "integrity": "sha512-+PnZ2u/BS+f5FiuHXz4zKsHPcMKHie+K+1Uvu/x91ovkCMEOJqEI8E9Tw1Wzx2QRz4MHOBHYf1ypO8N1K0aNAA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/utils": "^6.20.0"
+        "@typescript-eslint/utils": "^7.4.0"
       },
       "engines": {
         "node": "^18.0.0 || >= 20.0.0"
@@ -5069,22 +5779,23 @@
       }
     },
     "node_modules/eslint-plugin-vitest-globals": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest-globals/-/eslint-plugin-vitest-globals-1.4.0.tgz",
-      "integrity": "sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==",
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vitest-globals/-/eslint-plugin-vitest-globals-1.5.0.tgz",
+      "integrity": "sha512-ZSsVOaOIig0oVLzRTyk8lUfBfqzWxr/J3/NFMfGGRIkGQPejJYmDH3gXmSJxAojts77uzAGB/UmVrwi2DC4LYA==",
       "dev": true
     },
     "node_modules/eslint-plugin-vue": {
-      "version": "9.21.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.21.1.tgz",
-      "integrity": "sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw==",
+      "version": "9.24.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.24.0.tgz",
+      "integrity": "sha512-9SkJMvF8NGMT9aQCwFc5rj8Wo1XWSMSHk36i7ZwdI614BU7sIOR28ZjuFPKp8YGymZN12BSEbiSwa7qikp+PBw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
+        "globals": "^13.24.0",
         "natural-compare": "^1.4.0",
         "nth-check": "^2.1.1",
-        "postcss-selector-parser": "^6.0.13",
-        "semver": "^7.5.4",
+        "postcss-selector-parser": "^6.0.15",
+        "semver": "^7.6.0",
         "vue-eslint-parser": "^9.4.2",
         "xml-name-validator": "^4.0.0"
       },
@@ -5096,13 +5807,13 @@
       }
     },
     "node_modules/eslint-plugin-vue-scoped-css": {
-      "version": "2.7.2",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vue-scoped-css/-/eslint-plugin-vue-scoped-css-2.7.2.tgz",
-      "integrity": "sha512-myJ99CJuwmAx5kq1WjgIeaUkxeU6PIEUh7age79Alm30bhN4fVTapOQLSMlvVTgxr36Y3igsZ3BCJM32LbHHig==",
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue-scoped-css/-/eslint-plugin-vue-scoped-css-2.8.0.tgz",
+      "integrity": "sha512-JXb3Um4+AhuDGxSX6FAGCI0p811xF7W8L7yxC8wmAEZEI/teTjlpC09noqQZHXn53RZ/TGQJ8Onaq4teYLxBbg==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
-        "eslint-compat-utils": "^0.4.0",
+        "eslint-compat-utils": "^0.5.0",
         "lodash": "^4.17.21",
         "postcss": "^8.4.31",
         "postcss-safe-parser": "^6.0.0",
@@ -5134,6 +5845,15 @@
         "eslint": ">=5"
       }
     },
+    "node_modules/eslint-rule-documentation": {
+      "version": "1.0.23",
+      "resolved": "https://registry.npmjs.org/eslint-rule-documentation/-/eslint-rule-documentation-1.0.23.tgz",
+      "integrity": "sha512-pWReu3fkohwyvztx/oQWWgld2iad25TfUdi6wvhhaDPIQjHU/pyvlKgXFw1kX31SQK2Nq9MH+vRDWB0ZLy8fYw==",
+      "dev": true,
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
     "node_modules/eslint-scope": {
       "version": "7.2.2",
       "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -5371,9 +6091,9 @@
       }
     },
     "node_modules/fastq": {
-      "version": "1.17.0",
-      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz",
-      "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==",
+      "version": "1.17.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+      "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
       "dependencies": {
         "reusify": "^1.0.4"
       }
@@ -5405,25 +6125,6 @@
         }
       }
     },
-    "node_modules/fetch-ponyfill/node_modules/tr46": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
-    },
-    "node_modules/fetch-ponyfill/node_modules/webidl-conversions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
-    },
-    "node_modules/fetch-ponyfill/node_modules/whatwg-url": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
-      "dependencies": {
-        "tr46": "~0.0.3",
-        "webidl-conversions": "^3.0.0"
-      }
-    },
     "node_modules/file-entry-cache": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -5486,9 +6187,9 @@
       }
     },
     "node_modules/flatted": {
-      "version": "3.2.9",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
-      "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+      "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
       "dev": true
     },
     "node_modules/for-each": {
@@ -5504,7 +6205,6 @@
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
       "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
-      "dev": true,
       "dependencies": {
         "cross-spawn": "^7.0.0",
         "signal-exit": "^4.0.1"
@@ -5516,20 +6216,6 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/form-data": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
-      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
-      "dev": true,
-      "dependencies": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.8",
-        "mime-types": "^2.1.12"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
     "node_modules/fs-extra": {
       "version": "10.1.0",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -5553,7 +6239,6 @@
       "version": "2.3.2",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
       "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
-      "dev": true,
       "hasInstallScript": true,
       "optional": true,
       "os": [
@@ -5628,16 +6313,20 @@
       }
     },
     "node_modules/get-intrinsic": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
-      "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+      "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
       "dev": true,
       "dependencies": {
+        "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
         "has-proto": "^1.0.1",
         "has-symbols": "^1.0.3",
         "hasown": "^2.0.0"
       },
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -5686,13 +6375,14 @@
       }
     },
     "node_modules/get-symbol-description": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
-      "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
+      "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.1.1"
+        "call-bind": "^1.0.5",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.4"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5702,9 +6392,9 @@
       }
     },
     "node_modules/get-tsconfig": {
-      "version": "4.7.2",
-      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz",
-      "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==",
+      "version": "4.7.3",
+      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz",
+      "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==",
       "dependencies": {
         "resolve-pkg-maps": "^1.0.0"
       },
@@ -5735,7 +6425,6 @@
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
       "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-      "dev": true,
       "dependencies": {
         "is-glob": "^4.0.3"
       },
@@ -5896,6 +6585,28 @@
       "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz",
       "integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ=="
     },
+    "node_modules/hammerjs": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
+      "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/happy-dom": {
+      "version": "14.5.0",
+      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.5.0.tgz",
+      "integrity": "sha512-KvOtCq7eamc7cjihM0F1wj6FptuXzooc3Typa7Vgu6ns2uKGXC4BIFlK80SdH2w8zcW0gtxpBVI/sUqbYtljDA==",
+      "dev": true,
+      "dependencies": {
+        "entities": "^4.5.0",
+        "webidl-conversions": "^7.0.0",
+        "whatwg-mimetype": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
     "node_modules/has-bigints": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -5914,21 +6625,21 @@
       }
     },
     "node_modules/has-property-descriptors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
-      "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
       "dev": true,
       "dependencies": {
-        "get-intrinsic": "^1.2.2"
+        "es-define-property": "^1.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/has-proto": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
-      "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+      "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
       "dev": true,
       "engines": {
         "node": ">= 0.4"
@@ -5950,12 +6661,12 @@
       }
     },
     "node_modules/has-tostringtag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
-      "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
       "dev": true,
       "dependencies": {
-        "has-symbols": "^1.0.2"
+        "has-symbols": "^1.0.3"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5970,9 +6681,9 @@
       "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg=="
     },
     "node_modules/hasown": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
-      "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
       "dependencies": {
         "function-bind": "^1.1.2"
       },
@@ -6000,18 +6711,6 @@
         "node": ">=14"
       }
     },
-    "node_modules/html-encoding-sniffer": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
-      "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
-      "dev": true,
-      "dependencies": {
-        "whatwg-encoding": "^3.1.1"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
     "node_modules/html-tags": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
@@ -6044,35 +6743,9 @@
       }
     },
     "node_modules/htmx.org": {
-      "version": "1.9.10",
-      "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.10.tgz",
-      "integrity": "sha512-UgchasltTCrTuU2DQLom3ohHrBvwr7OqpwyAVJ9VxtNBng4XKkVsqrv0Qr3srqvM9ZNI3f1MmvVQQqK7KW/bTA=="
-    },
-    "node_modules/http-proxy-agent": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
-      "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==",
-      "dev": true,
-      "dependencies": {
-        "agent-base": "^7.1.0",
-        "debug": "^4.3.4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
-    "node_modules/https-proxy-agent": {
-      "version": "7.0.2",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
-      "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
-      "dev": true,
-      "dependencies": {
-        "agent-base": "^7.0.2",
-        "debug": "4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
+      "version": "1.9.11",
+      "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.11.tgz",
+      "integrity": "sha512-WlVuICn8dfNOOgYmdYzYG8zSnP3++AdHkMHooQAzGZObWpVXYathpz/I37ycF4zikR6YduzfCvEcxk20JkIUsw=="
     },
     "node_modules/human-signals": {
       "version": "5.0.0",
@@ -6105,6 +6778,11 @@
         "postcss": "^8.1.0"
       }
     },
+    "node_modules/idiomorph": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.3.0.tgz",
+      "integrity": "sha512-UhV1Ey5xCxIwR9B+OgIjQa+1Jx99XQ1vQHUsKBU1RpQzCx1u+b+N6SOXgf5mEJDqemUI/ffccu6+71l2mJUsRA=="
+    },
     "node_modules/ieee754": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -6125,9 +6803,9 @@
       ]
     },
     "node_modules/ignore": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
-      "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+      "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
       "dev": true,
       "engines": {
         "node": ">= 4"
@@ -6147,7 +6825,6 @@
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
       "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
-      "dev": true,
       "dependencies": {
         "parent-module": "^1.0.0",
         "resolve-from": "^4.0.0"
@@ -6210,21 +6887,21 @@
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
     "node_modules/ini": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
-      "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz",
+      "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==",
       "dev": true,
       "engines": {
         "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
       }
     },
     "node_modules/internal-slot": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
-      "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
+      "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
       "dev": true,
       "dependencies": {
-        "get-intrinsic": "^1.2.2",
+        "es-errors": "^1.3.0",
         "hasown": "^2.0.0",
         "side-channel": "^1.0.4"
       },
@@ -6249,14 +6926,16 @@
       }
     },
     "node_modules/is-array-buffer": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
-      "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
+      "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.0",
-        "is-typed-array": "^1.1.10"
+        "get-intrinsic": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -6265,8 +6944,22 @@
     "node_modules/is-arrayish": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
-      "dev": true
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
+    },
+    "node_modules/is-async-function": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
+      "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==",
+      "dev": true,
+      "dependencies": {
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
     "node_modules/is-bigint": {
       "version": "1.0.4",
@@ -6280,6 +6973,17 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/is-boolean-object": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
@@ -6334,6 +7038,21 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-data-view": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz",
+      "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==",
+      "dev": true,
+      "dependencies": {
+        "is-typed-array": "^1.1.13"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-date-object": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
@@ -6357,6 +7076,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-finalizationregistry": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz",
+      "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-fullwidth-code-point": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -6365,6 +7096,21 @@
         "node": ">=8"
       }
     },
+    "node_modules/is-generator-function": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+      "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+      "dev": true,
+      "dependencies": {
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-get-set-prop": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-get-set-prop/-/is-get-set-prop-1.0.0.tgz",
@@ -6395,10 +7141,22 @@
         "js-types": "^1.0.0"
       }
     },
+    "node_modules/is-map": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+      "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-negative-zero": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
-      "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+      "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
       "dev": true,
       "engines": {
         "node": ">= 0.4"
@@ -6499,13 +7257,28 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-set": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+      "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-shared-array-buffer": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
-      "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
+      "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2"
+        "call-bind": "^1.0.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -6554,12 +7327,12 @@
       }
     },
     "node_modules/is-typed-array": {
-      "version": "1.1.12",
-      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
-      "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
+      "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
       "dev": true,
       "dependencies": {
-        "which-typed-array": "^1.1.11"
+        "which-typed-array": "^1.1.14"
       },
       "engines": {
         "node": ">= 0.4"
@@ -6577,6 +7350,18 @@
         "is-potential-custom-element-name": "^1.0.0"
       }
     },
+    "node_modules/is-weakmap": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+      "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-weakref": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@@ -6589,6 +7374,22 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-weakset": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz",
+      "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "get-intrinsic": "^1.2.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/isarray": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@@ -6608,11 +7409,23 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/iterator.prototype": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
+      "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==",
+      "dev": true,
+      "dependencies": {
+        "define-properties": "^1.2.1",
+        "get-intrinsic": "^1.2.1",
+        "has-symbols": "^1.0.3",
+        "reflect.getprototypeof": "^1.0.4",
+        "set-function-name": "^2.0.1"
+      }
+    },
     "node_modules/jackspeak": {
       "version": "2.3.6",
       "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
       "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
-      "dev": true,
       "dependencies": {
         "@isaacs/cliui": "^8.0.2"
       },
@@ -6653,6 +7466,14 @@
         "url": "https://github.com/chalk/supports-color?sponsor=1"
       }
     },
+    "node_modules/jiti": {
+      "version": "1.21.0",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
+      "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
     "node_modules/jquery": {
       "version": "3.7.1",
       "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
@@ -6665,9 +7486,9 @@
       "dev": true
     },
     "node_modules/js-tokens": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.2.tgz",
-      "integrity": "sha512-Olnt+V7xYdvGze9YTbGFZIfQXuGV4R3nQwwl8BrtgaPE/wq8UFpUHWuTNc05saowhSr1ZO6tx+V6RjE9D5YQog==",
+      "version": "8.0.3",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz",
+      "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==",
       "dev": true
     },
     "node_modules/js-types": {
@@ -6699,55 +7520,6 @@
         "node": ">=12.0.0"
       }
     },
-    "node_modules/jsdom": {
-      "version": "24.0.0",
-      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz",
-      "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==",
-      "dev": true,
-      "dependencies": {
-        "cssstyle": "^4.0.1",
-        "data-urls": "^5.0.0",
-        "decimal.js": "^10.4.3",
-        "form-data": "^4.0.0",
-        "html-encoding-sniffer": "^4.0.0",
-        "http-proxy-agent": "^7.0.0",
-        "https-proxy-agent": "^7.0.2",
-        "is-potential-custom-element-name": "^1.0.1",
-        "nwsapi": "^2.2.7",
-        "parse5": "^7.1.2",
-        "rrweb-cssom": "^0.6.0",
-        "saxes": "^6.0.0",
-        "symbol-tree": "^3.2.4",
-        "tough-cookie": "^4.1.3",
-        "w3c-xmlserializer": "^5.0.0",
-        "webidl-conversions": "^7.0.0",
-        "whatwg-encoding": "^3.1.1",
-        "whatwg-mimetype": "^4.0.0",
-        "whatwg-url": "^14.0.0",
-        "ws": "^8.16.0",
-        "xml-name-validator": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=18"
-      },
-      "peerDependencies": {
-        "canvas": "^2.11.2"
-      },
-      "peerDependenciesMeta": {
-        "canvas": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/jsdom/node_modules/xml-name-validator": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
-      "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
-      "dev": true,
-      "engines": {
-        "node": ">=18"
-      }
-    },
     "node_modules/jsep": {
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.8.tgz",
@@ -6838,15 +7610,30 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/jsx-ast-utils": {
+      "version": "3.3.5",
+      "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+      "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+      "dev": true,
+      "dependencies": {
+        "array-includes": "^3.1.6",
+        "array.prototype.flat": "^1.3.1",
+        "object.assign": "^4.1.4",
+        "object.values": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
     "node_modules/just-extend": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
       "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ=="
     },
     "node_modules/katex": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz",
-      "integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==",
+      "version": "0.16.10",
+      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
+      "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
       "funding": [
         "https://opencollective.com/katex",
         "https://github.com/sponsors/katex"
@@ -6889,11 +7676,29 @@
       }
     },
     "node_modules/known-css-properties": {
-      "version": "0.29.0",
-      "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz",
-      "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==",
+      "version": "0.30.0",
+      "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.30.0.tgz",
+      "integrity": "sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==",
       "dev": true
     },
+    "node_modules/language-subtag-registry": {
+      "version": "0.3.22",
+      "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
+      "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+      "dev": true
+    },
+    "node_modules/language-tags": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+      "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
+      "dev": true,
+      "dependencies": {
+        "language-subtag-registry": "^0.3.20"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
     "node_modules/layout-base": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
@@ -6981,11 +7786,18 @@
         "node": ">=8"
       }
     },
+    "node_modules/lilconfig": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+      "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/lines-and-columns": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
-      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
-      "dev": true
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
     },
     "node_modules/linkify-it": {
       "version": "5.0.0",
@@ -7063,12 +7875,30 @@
       "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
       "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA=="
     },
+    "node_modules/lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+      "dev": true
+    },
+    "node_modules/lodash.kebabcase": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
+      "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==",
+      "dev": true
+    },
     "node_modules/lodash.merge": {
       "version": "4.6.2",
       "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
       "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
       "dev": true
     },
+    "node_modules/lodash.snakecase": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
+      "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
+      "dev": true
+    },
     "node_modules/lodash.sortedlastindex": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/lodash.sortedlastindex/-/lodash.sortedlastindex-4.1.0.tgz",
@@ -7104,6 +7934,12 @@
       "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
       "dev": true
     },
+    "node_modules/lodash.upperfirst": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz",
+      "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==",
+      "dev": true
+    },
     "node_modules/loupe": {
       "version": "2.3.7",
       "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
@@ -7208,16 +8044,16 @@
       }
     },
     "node_modules/markdownlint-cli/node_modules/glob": {
-      "version": "10.3.10",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
-      "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+      "version": "10.3.12",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
+      "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
       "dev": true,
       "dependencies": {
         "foreground-child": "^3.1.0",
-        "jackspeak": "^2.3.5",
+        "jackspeak": "^2.3.6",
         "minimatch": "^9.0.1",
-        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
-        "path-scurry": "^1.10.1"
+        "minipass": "^7.0.4",
+        "path-scurry": "^1.10.2"
       },
       "bin": {
         "glob": "dist/esm/bin.mjs"
@@ -7316,9 +8152,9 @@
       "dev": true
     },
     "node_modules/meow": {
-      "version": "13.1.0",
-      "resolved": "https://registry.npmjs.org/meow/-/meow-13.1.0.tgz",
-      "integrity": "sha512-o5R/R3Tzxq0PJ3v3qcQJtSvSE9nKOLSAaDuuoMzDVuGTwHdccMWcYomh9Xolng2tjT6O/Y83d+0coVGof6tqmA==",
+      "version": "13.2.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
+      "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
       "dev": true,
       "engines": {
         "node": ">=18"
@@ -7341,22 +8177,22 @@
       }
     },
     "node_modules/mermaid": {
-      "version": "10.7.0",
-      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.7.0.tgz",
-      "integrity": "sha512-PsvGupPCkN1vemAAjScyw4pw34p4/0dZkSrqvAB26hUvJulOWGIwt35FZWmT9wPIi4r0QLa5X0PB4YLIGn0/YQ==",
+      "version": "10.9.0",
+      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.0.tgz",
+      "integrity": "sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==",
       "dependencies": {
         "@braintree/sanitize-url": "^6.0.1",
         "@types/d3-scale": "^4.0.3",
         "@types/d3-scale-chromatic": "^3.0.0",
-        "cytoscape": "^3.23.0",
+        "cytoscape": "^3.28.1",
         "cytoscape-cose-bilkent": "^4.1.0",
-        "cytoscape-fcose": "^2.1.0",
         "d3": "^7.4.0",
         "d3-sankey": "^0.12.3",
         "dagre-d3-es": "7.0.10",
         "dayjs": "^1.11.7",
         "dompurify": "^3.0.5",
         "elkjs": "^0.9.0",
+        "katex": "^0.16.9",
         "khroma": "^2.0.0",
         "lodash-es": "^4.17.21",
         "mdast-util-from-markdown": "^1.3.0",
@@ -7841,9 +8677,9 @@
       }
     },
     "node_modules/mini-css-extract-plugin": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.0.tgz",
-      "integrity": "sha512-CxmUYPFcTgET1zImteG/LZOy/4T5rTojesQXkSNBiquhydn78tfbCE9sjIjnJ/UcjNjOC1bphTCCW5rrS7cXAg==",
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz",
+      "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==",
       "dependencies": {
         "schema-utils": "^4.0.0",
         "tapable": "^2.2.1"
@@ -7860,9 +8696,9 @@
       }
     },
     "node_modules/minimatch": {
-      "version": "9.0.3",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
-      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "version": "9.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+      "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
       "dependencies": {
         "brace-expansion": "^2.0.1"
       },
@@ -7886,15 +8722,14 @@
       "version": "7.0.4",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
       "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
-      "dev": true,
       "engines": {
         "node": ">=16 || 14 >=14.17"
       }
     },
     "node_modules/mlly": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz",
-      "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==",
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz",
+      "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==",
       "dev": true,
       "dependencies": {
         "acorn": "^8.11.3",
@@ -7904,9 +8739,9 @@
       }
     },
     "node_modules/monaco-editor": {
-      "version": "0.45.0",
-      "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.45.0.tgz",
-      "integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA=="
+      "version": "0.47.0",
+      "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.47.0.tgz",
+      "integrity": "sha512-VabVvHvQ9QmMwXu4du008ZDuyLnHs9j7ThVFsiJoXSOQk18+LF89N4ADzPbFenm0W4V2bGHnFBztIRQTgBfxzw=="
     },
     "node_modules/monaco-editor-webpack-plugin": {
       "version": "7.1.0",
@@ -7938,6 +8773,16 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
     "node_modules/nanoid": {
       "version": "3.3.7",
       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -8014,25 +8859,6 @@
         }
       }
     },
-    "node_modules/node-fetch/node_modules/tr46": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
-    },
-    "node_modules/node-fetch/node_modules/webidl-conversions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
-    },
-    "node_modules/node-fetch/node_modules/whatwg-url": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
-      "dependencies": {
-        "tr46": "~0.0.3",
-        "webidl-conversions": "^3.0.0"
-      }
-    },
     "node_modules/node-releases": {
       "version": "2.0.14",
       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@@ -8081,15 +8907,14 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
       "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
     },
     "node_modules/npm-run-path": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
-      "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+      "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
       "dev": true,
       "dependencies": {
         "path-key": "^4.0.0"
@@ -8125,12 +8950,6 @@
         "url": "https://github.com/fb55/nth-check?sponsor=1"
       }
     },
-    "node_modules/nwsapi": {
-      "version": "2.2.7",
-      "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz",
-      "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==",
-      "dev": true
-    },
     "node_modules/obj-props": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/obj-props/-/obj-props-1.4.0.tgz",
@@ -8148,6 +8967,14 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/object-inspect": {
       "version": "1.13.1",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
@@ -8184,6 +9011,69 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/object.entries": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz",
+      "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/object.fromentries": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+      "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object.groupby": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+      "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/object.values": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz",
+      "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -8266,7 +9156,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
       "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
-      "dev": true,
       "dependencies": {
         "callsites": "^3.0.0"
       },
@@ -8278,7 +9167,6 @@
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
       "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
-      "dev": true,
       "dependencies": {
         "@babel/code-frame": "^7.0.0",
         "error-ex": "^1.3.1",
@@ -8303,18 +9191,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/parse5": {
-      "version": "7.1.2",
-      "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
-      "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
-      "dev": true,
-      "dependencies": {
-        "entities": "^4.4.0"
-      },
-      "funding": {
-        "url": "https://github.com/inikulin/parse5?sponsor=1"
-      }
-    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -8345,12 +9221,11 @@
       "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
     },
     "node_modules/path-scurry": {
-      "version": "1.10.1",
-      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
-      "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
-      "dev": true,
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz",
+      "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==",
       "dependencies": {
-        "lru-cache": "^9.1.1 || ^10.0.0",
+        "lru-cache": "^10.2.0",
         "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
       },
       "engines": {
@@ -8364,7 +9239,6 @@
       "version": "10.2.0",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
       "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
-      "dev": true,
       "engines": {
         "node": "14 || >=16.14"
       }
@@ -8394,9 +9268,9 @@
       }
     },
     "node_modules/pdfobject": {
-      "version": "2.2.12",
-      "resolved": "https://registry.npmjs.org/pdfobject/-/pdfobject-2.2.12.tgz",
-      "integrity": "sha512-D0oyD/sj8j82AMaJhoyMaY1aD5TkbpU3FbJC6w9/cpJlZRpYHqAkutXw1Ca/FKjYPZmTAu58uGIfgOEaDlbY8A=="
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pdfobject/-/pdfobject-2.3.0.tgz",
+      "integrity": "sha512-w/9pXDXTDs3IDmOri/w8lM/w6LHR0/F4fcBLLzH+4csSoyshQ5su0TE7k0FLHZO7aOjVLDGecqd1M89+PVpVAA=="
     },
     "node_modules/picocolors": {
       "version": "1.0.0",
@@ -8414,6 +9288,22 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+      "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/pkg-dir": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@@ -8491,12 +9381,12 @@
       "dev": true
     },
     "node_modules/playwright": {
-      "version": "1.41.1",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.1.tgz",
-      "integrity": "sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==",
+      "version": "1.42.1",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
+      "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
       "dev": true,
       "dependencies": {
-        "playwright-core": "1.41.1"
+        "playwright-core": "1.42.1"
       },
       "bin": {
         "playwright": "cli.js"
@@ -8509,9 +9399,9 @@
       }
     },
     "node_modules/playwright-core": {
-      "version": "1.41.1",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.1.tgz",
-      "integrity": "sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==",
+      "version": "1.42.1",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
+      "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==",
       "dev": true,
       "bin": {
         "playwright-core": "cli.js"
@@ -8538,10 +9428,19 @@
         "node": ">=12.0.0"
       }
     },
+    "node_modules/possible-typed-array-names": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
+      "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/postcss": {
-      "version": "8.4.33",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
-      "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
+      "version": "8.4.38",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+      "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
       "funding": [
         {
           "type": "opencollective",
@@ -8559,7 +9458,7 @@
       "dependencies": {
         "nanoid": "^3.3.7",
         "picocolors": "^1.0.0",
-        "source-map-js": "^1.0.2"
+        "source-map-js": "^1.2.0"
       },
       "engines": {
         "node": "^10 || ^12 || >=14"
@@ -8580,10 +9479,74 @@
         "node": "^12 || >=14"
       }
     },
+    "node_modules/postcss-import": {
+      "version": "15.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-js": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+      "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+      "dependencies": {
+        "camelcase-css": "^2.0.1"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >= 16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.21"
+      }
+    },
+    "node_modules/postcss-loader": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz",
+      "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==",
+      "dependencies": {
+        "cosmiconfig": "^9.0.0",
+        "jiti": "^1.20.0",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": ">= 18.12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "@rspack/core": "0.x || 1.x",
+        "postcss": "^7.0.0 || ^8.0.1",
+        "webpack": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@rspack/core": {
+          "optional": true
+        },
+        "webpack": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/postcss-modules-extract-imports": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
-      "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
+      "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
       "engines": {
         "node": "^10 || ^12 || >= 14"
       },
@@ -8592,9 +9555,9 @@
       }
     },
     "node_modules/postcss-modules-local-by-default": {
-      "version": "4.0.4",
-      "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz",
-      "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==",
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz",
+      "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==",
       "dependencies": {
         "icss-utils": "^5.0.0",
         "postcss-selector-parser": "^6.0.2",
@@ -8608,9 +9571,9 @@
       }
     },
     "node_modules/postcss-modules-scope": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz",
-      "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz",
+      "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==",
       "dependencies": {
         "postcss-selector-parser": "^6.0.4"
       },
@@ -8635,6 +9598,50 @@
         "postcss": "^8.1.0"
       }
     },
+    "node_modules/postcss-nested": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
+      "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.11"
+      },
+      "engines": {
+        "node": ">=12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2.14"
+      }
+    },
+    "node_modules/postcss-nesting": {
+      "version": "12.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.1.tgz",
+      "integrity": "sha512-qc74KvIAQNa5ujZKG1UV286dhaDW6basbUy2i9AzNU/T8C9hpvGu9NZzm1SfePe2yP7sPYgpA8d4sPVopn2Hhw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "dependencies": {
+        "@csstools/selector-resolve-nested": "^1.1.0",
+        "@csstools/selector-specificity": "^3.0.3",
+        "postcss-selector-parser": "^6.0.13"
+      },
+      "engines": {
+        "node": "^14 || ^16 || >=18"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
     "node_modules/postcss-resolve-nested-selector": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz",
@@ -8684,9 +9691,9 @@
       }
     },
     "node_modules/postcss-selector-parser": {
-      "version": "6.0.15",
-      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz",
-      "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==",
+      "version": "6.0.16",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
+      "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
       "dependencies": {
         "cssesc": "^3.0.0",
         "util-deprecate": "^1.0.2"
@@ -8728,6 +9735,33 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/prettier": {
+      "version": "3.2.5",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
+      "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
+      "dev": true,
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/prettier-linter-helpers": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+      "dev": true,
+      "dependencies": {
+        "fast-diff": "^1.1.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
     "node_modules/pretty-format": {
       "version": "29.7.0",
       "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
@@ -8783,12 +9817,6 @@
         "node": ">=4"
       }
     },
-    "node_modules/psl": {
-      "version": "1.9.0",
-      "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
-      "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
-      "dev": true
-    },
     "node_modules/punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -8806,12 +9834,6 @@
         "node": ">=6"
       }
     },
-    "node_modules/querystringify": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
-      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
-      "dev": true
-    },
     "node_modules/queue-microtask": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -8845,6 +9867,14 @@
       "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
       "dev": true
     },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
     "node_modules/read-pkg": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@@ -8947,6 +9977,17 @@
         "node": ">=8"
       }
     },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
     "node_modules/rechoir": {
       "version": "0.8.0",
       "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
@@ -8970,6 +10011,27 @@
         "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
       }
     },
+    "node_modules/reflect.getprototypeof": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
+      "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.1",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.4",
+        "globalthis": "^1.0.3",
+        "which-builtin-type": "^1.1.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/regenerator-runtime": {
       "version": "0.14.1",
       "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@@ -8998,14 +10060,15 @@
       }
     },
     "node_modules/regexp.prototype.flags": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
-      "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
+      "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "set-function-name": "^2.0.0"
+        "call-bind": "^1.0.6",
+        "define-properties": "^1.2.1",
+        "es-errors": "^1.3.0",
+        "set-function-name": "^2.0.1"
       },
       "engines": {
         "node": ">= 0.4"
@@ -9052,12 +10115,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/requires-port": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
-      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
-      "dev": true
-    },
     "node_modules/reserved": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/reserved/-/reserved-0.1.2.tgz",
@@ -9106,7 +10163,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
       "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
-      "dev": true,
       "engines": {
         "node": ">=4"
       }
@@ -9163,12 +10219,6 @@
         "fsevents": "~2.3.2"
       }
     },
-    "node_modules/rrweb-cssom": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
-      "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==",
-      "dev": true
-    },
     "node_modules/run-con": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.3.2.tgz",
@@ -9223,13 +10273,13 @@
       }
     },
     "node_modules/safe-array-concat": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz",
-      "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
+      "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.5",
-        "get-intrinsic": "^1.2.2",
+        "call-bind": "^1.0.7",
+        "get-intrinsic": "^1.2.4",
         "has-symbols": "^1.0.3",
         "isarray": "^2.0.5"
       },
@@ -9260,13 +10310,13 @@
       ]
     },
     "node_modules/safe-regex-test": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
-      "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
+      "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.5",
-        "get-intrinsic": "^1.2.2",
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
         "is-regex": "^1.1.4"
       },
       "engines": {
@@ -9293,18 +10343,6 @@
       "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
       "dev": true
     },
-    "node_modules/saxes": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
-      "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
-      "dev": true,
-      "dependencies": {
-        "xmlchars": "^2.2.0"
-      },
-      "engines": {
-        "node": ">=v12.22.7"
-      }
-    },
     "node_modules/schema-utils": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
@@ -9338,9 +10376,9 @@
       }
     },
     "node_modules/semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
       "dependencies": {
         "lru-cache": "^6.0.0"
       },
@@ -9360,17 +10398,17 @@
       }
     },
     "node_modules/seroval": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.0.4.tgz",
-      "integrity": "sha512-qQs/N+KfJu83rmszFQaTxcoJoPn6KNUruX4KmnmyD0oZkUoiNvJ1rpdYKDf4YHM05k+HOgCxa3yvf15QbVijGg==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.0.5.tgz",
+      "integrity": "sha512-TM+Z11tHHvQVQKeNlOUonOWnsNM+2IBwZ4vwoi4j3zKzIpc5IDw8WPwCfcc8F17wy6cBcJGbZbFOR0UCuTZHQA==",
       "engines": {
         "node": ">=10"
       }
     },
     "node_modules/seroval-plugins": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.0.4.tgz",
-      "integrity": "sha512-DQ2IK6oQVvy8k+c2V5x5YCtUa/GGGsUwUBNN9UqohrZ0rWdUapBFpNMYP1bCyRHoxOJjdKGl+dieacFIpU/i1A==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.0.5.tgz",
+      "integrity": "sha512-8+pDC1vOedPXjKG7oz8o+iiHrtF2WswaMQJ7CKFpccvSYfrzmvKY9zOJWCg+881722wIHfwkdnRmiiDm9ym+zQ==",
       "engines": {
         "node": ">=10"
       },
@@ -9379,30 +10417,32 @@
       }
     },
     "node_modules/set-function-length": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
-      "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
       "dev": true,
       "dependencies": {
-        "define-data-property": "^1.1.1",
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.2",
+        "get-intrinsic": "^1.2.4",
         "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.1"
+        "has-property-descriptors": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/set-function-name": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
-      "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+      "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
       "dev": true,
       "dependencies": {
-        "define-data-property": "^1.0.1",
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
         "functions-have-names": "^1.2.3",
-        "has-property-descriptors": "^1.0.0"
+        "has-property-descriptors": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -9439,14 +10479,18 @@
       }
     },
     "node_modules/side-channel": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
-      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+      "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.0",
-        "get-intrinsic": "^1.0.2",
-        "object-inspect": "^1.9.0"
+        "call-bind": "^1.0.7",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.4",
+        "object-inspect": "^1.13.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -9462,7 +10506,6 @@
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
       "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
-      "dev": true,
       "engines": {
         "node": ">=14"
       },
@@ -9509,12 +10552,12 @@
       }
     },
     "node_modules/solid-js": {
-      "version": "1.8.12",
-      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.12.tgz",
-      "integrity": "sha512-sLE/i6M9FSWlov3a2pTC5ISzanH2aKwqXTZj+bbFt4SUrVb4iGEa7fpILBMOxsQjkv3eXqEk6JVLlogOdTe0UQ==",
+      "version": "1.8.16",
+      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.16.tgz",
+      "integrity": "sha512-rja94MNU9flF3qQRLNsu60QHKBDKBkVE1DldJZPIfn2ypIn3NV2WpSbGTQIvsyGPBo+9E2IMjwqnqpbgfWuzeg==",
       "dependencies": {
         "csstype": "^3.1.0",
-        "seroval": "^1.0.3",
+        "seroval": "^1.0.4",
         "seroval-plugins": "^1.0.3"
       }
     },
@@ -9537,9 +10580,9 @@
       }
     },
     "node_modules/source-map-js": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
-      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
+      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -9592,9 +10635,9 @@
       }
     },
     "node_modules/spdx-exceptions": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz",
-      "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw=="
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+      "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="
     },
     "node_modules/spdx-expression-parse": {
       "version": "3.0.1",
@@ -9614,9 +10657,9 @@
       }
     },
     "node_modules/spdx-license-ids": {
-      "version": "3.0.16",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
-      "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw=="
+      "version": "3.0.17",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz",
+      "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg=="
     },
     "node_modules/spdx-ranges": {
       "version": "2.1.1",
@@ -9673,7 +10716,6 @@
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "dev": true,
       "dependencies": {
         "emoji-regex": "^8.0.0",
         "is-fullwidth-code-point": "^3.0.0",
@@ -9683,15 +10725,26 @@
         "node": ">=8"
       }
     },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/string-width/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
     "node_modules/string.prototype.trim": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
-      "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz",
+      "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.0",
+        "es-object-atoms": "^1.0.0"
       },
       "engines": {
         "node": ">= 0.4"
@@ -9701,28 +10754,31 @@
       }
     },
     "node_modules/string.prototype.trimend": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
-      "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz",
+      "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/string.prototype.trimstart": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
-      "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+      "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -9744,7 +10800,6 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
       "dependencies": {
         "ansi-regex": "^5.0.1"
       },
@@ -9752,6 +10807,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/strip-final-newline": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
@@ -9789,17 +10853,23 @@
       }
     },
     "node_modules/strip-literal": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz",
-      "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz",
+      "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==",
       "dev": true,
       "dependencies": {
-        "acorn": "^8.10.0"
+        "js-tokens": "^9.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/antfu"
       }
     },
+    "node_modules/strip-literal/node_modules/js-tokens": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz",
+      "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==",
+      "dev": true
+    },
     "node_modules/style-search": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
@@ -9807,15 +10877,16 @@
       "dev": true
     },
     "node_modules/stylelint": {
-      "version": "16.2.1",
-      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.2.1.tgz",
-      "integrity": "sha512-SfIMGFK+4n7XVAyv50CpVfcGYWG4v41y6xG7PqOgQSY8M/PgdK0SQbjWFblxjJZlN9jNq879mB4BCZHJRIJ1hA==",
+      "version": "16.3.1",
+      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.3.1.tgz",
+      "integrity": "sha512-/JOwQnBvxEKOT2RtNgGpBVXnCSMBgKOL2k7w0K52htwCyJls4+cHvc4YZgXlVoAZS9QJd2DgYAiRnja96pTgxw==",
       "dev": true,
       "dependencies": {
-        "@csstools/css-parser-algorithms": "^2.5.0",
-        "@csstools/css-tokenizer": "^2.2.3",
-        "@csstools/media-query-list-parser": "^2.1.7",
-        "@csstools/selector-specificity": "^3.0.1",
+        "@csstools/css-parser-algorithms": "^2.6.1",
+        "@csstools/css-tokenizer": "^2.2.4",
+        "@csstools/media-query-list-parser": "^2.1.9",
+        "@csstools/selector-specificity": "^3.0.2",
+        "@dual-bundle/import-meta-resolve": "^4.0.0",
         "balanced-match": "^2.0.0",
         "colord": "^2.9.3",
         "cosmiconfig": "^9.0.0",
@@ -9829,19 +10900,19 @@
         "globby": "^11.1.0",
         "globjoin": "^0.1.4",
         "html-tags": "^3.3.1",
-        "ignore": "^5.3.0",
+        "ignore": "^5.3.1",
         "imurmurhash": "^0.1.4",
         "is-plain-object": "^5.0.0",
-        "known-css-properties": "^0.29.0",
+        "known-css-properties": "^0.30.0",
         "mathml-tag-names": "^2.1.3",
-        "meow": "^13.1.0",
+        "meow": "^13.2.0",
         "micromatch": "^4.0.5",
         "normalize-path": "^3.0.0",
         "picocolors": "^1.0.0",
-        "postcss": "^8.4.33",
+        "postcss": "^8.4.38",
         "postcss-resolve-nested-selector": "^0.1.1",
         "postcss-safe-parser": "^7.0.0",
-        "postcss-selector-parser": "^6.0.15",
+        "postcss-selector-parser": "^6.0.16",
         "postcss-value-parser": "^4.2.0",
         "resolve-from": "^5.0.0",
         "string-width": "^4.2.3",
@@ -9886,6 +10957,22 @@
         "stylelint": ">=7 <=16"
       }
     },
+    "node_modules/stylelint-value-no-unknown-custom-properties": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/stylelint-value-no-unknown-custom-properties/-/stylelint-value-no-unknown-custom-properties-6.0.1.tgz",
+      "integrity": "sha512-N60PTdaTknB35j6D4FhW0GL2LlBRV++bRpXMMldWMQZ240yFQaoltzlLY4lXXs7Z0J5mNUYZQ/gjyVtU2DhCMA==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0",
+        "resolve": "^1.22.8"
+      },
+      "engines": {
+        "node": ">=18.12.0"
+      },
+      "peerDependencies": {
+        "stylelint": ">=16"
+      }
+    },
     "node_modules/stylelint/node_modules/ansi-regex": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
@@ -9917,41 +11004,18 @@
       }
     },
     "node_modules/stylelint/node_modules/flat-cache": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.0.tgz",
-      "integrity": "sha512-EryKbCE/wxpxKniQlyas6PY1I9vwtF3uCBweX+N8KYTCn3Y12RTGtQAJ/bd5pl7kxUAc8v/R3Ake/N17OZiFqA==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
       "dev": true,
       "dependencies": {
         "flatted": "^3.2.9",
-        "keyv": "^4.5.4",
-        "rimraf": "^5.0.5"
+        "keyv": "^4.5.4"
       },
       "engines": {
         "node": ">=16"
       }
     },
-    "node_modules/stylelint/node_modules/glob": {
-      "version": "10.3.10",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
-      "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
-      "dev": true,
-      "dependencies": {
-        "foreground-child": "^3.1.0",
-        "jackspeak": "^2.3.5",
-        "minimatch": "^9.0.1",
-        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
-        "path-scurry": "^1.10.1"
-      },
-      "bin": {
-        "glob": "dist/esm/bin.mjs"
-      },
-      "engines": {
-        "node": ">=16 || 14 >=14.17"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
     "node_modules/stylelint/node_modules/postcss-safe-parser": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.0.tgz",
@@ -9987,24 +11051,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/stylelint/node_modules/rimraf": {
-      "version": "5.0.5",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
-      "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
-      "dev": true,
-      "dependencies": {
-        "glob": "^10.3.7"
-      },
-      "bin": {
-        "rimraf": "dist/esm/bin.mjs"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
     "node_modules/stylelint/node_modules/strip-ansi": {
       "version": "7.1.0",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
@@ -10054,6 +11100,56 @@
         "node": ">= 8"
       }
     },
+    "node_modules/sucrase": {
+      "version": "3.35.0",
+      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+      "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "commander": "^4.0.0",
+        "glob": "^10.3.10",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "bin": {
+        "sucrase": "bin/sucrase",
+        "sucrase-node": "bin/sucrase-node"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/sucrase/node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/sucrase/node_modules/glob": {
+      "version": "10.3.12",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
+      "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^2.3.6",
+        "minimatch": "^9.0.1",
+        "minipass": "^7.0.4",
+        "path-scurry": "^1.10.2"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/superstruct": {
       "version": "0.10.13",
       "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.10.13.tgz",
@@ -10094,6 +11190,16 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/svg-element-attributes": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/svg-element-attributes/-/svg-element-attributes-1.3.1.tgz",
+      "integrity": "sha512-Bh05dSOnJBf3miNMqpsormfNtfidA/GxQVakhtn0T4DECWKeXQRQUceYjJ+OxYiiLdGe4Jo9iFV8wICFapFeIA==",
+      "dev": true,
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
     "node_modules/svg-tags": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
@@ -10135,15 +11241,9 @@
       }
     },
     "node_modules/swagger-ui-dist": {
-      "version": "5.11.2",
-      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz",
-      "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A=="
-    },
-    "node_modules/symbol-tree": {
-      "version": "3.2.4",
-      "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
-      "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
-      "dev": true
+      "version": "5.13.0",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.13.0.tgz",
+      "integrity": "sha512-uaWhh6j18IIs5tOX0arvIBnVINAzpTXaQXkr7qAk8zoupegJVg0UU/5+S/FgsgVCnzVsJ9d7QLjIxkswEeTg0Q=="
     },
     "node_modules/sync-fetch": {
       "version": "0.4.5",
@@ -10157,10 +11257,26 @@
         "node": ">=14"
       }
     },
+    "node_modules/synckit": {
+      "version": "0.8.8",
+      "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz",
+      "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==",
+      "dev": true,
+      "dependencies": {
+        "@pkgr/core": "^0.1.0",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/unts"
+      }
+    },
     "node_modules/table": {
-      "version": "6.8.1",
-      "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
-      "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
+      "version": "6.8.2",
+      "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz",
+      "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==",
       "dev": true,
       "dependencies": {
         "ajv": "^8.0.1",
@@ -10173,6 +11289,87 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/tailwindcss": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
+      "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
+      "dependencies": {
+        "@alloc/quick-lru": "^5.2.0",
+        "arg": "^5.0.2",
+        "chokidar": "^3.5.3",
+        "didyoumean": "^1.2.2",
+        "dlv": "^1.1.3",
+        "fast-glob": "^3.3.0",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
+        "jiti": "^1.21.0",
+        "lilconfig": "^2.1.0",
+        "micromatch": "^4.0.5",
+        "normalize-path": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "picocolors": "^1.0.0",
+        "postcss": "^8.4.23",
+        "postcss-import": "^15.1.0",
+        "postcss-js": "^4.0.1",
+        "postcss-load-config": "^4.0.1",
+        "postcss-nested": "^6.0.1",
+        "postcss-selector-parser": "^6.0.11",
+        "resolve": "^1.22.2",
+        "sucrase": "^3.32.0"
+      },
+      "bin": {
+        "tailwind": "lib/cli.js",
+        "tailwindcss": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/tailwindcss/node_modules/postcss-load-config": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+      "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "lilconfig": "^3.0.0",
+        "yaml": "^2.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      },
+      "peerDependencies": {
+        "postcss": ">=8.0.9",
+        "ts-node": ">=9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "postcss": {
+          "optional": true
+        },
+        "ts-node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
+      "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antonk52"
+      }
+    },
     "node_modules/tapable": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -10181,10 +11378,23 @@
         "node": ">=6"
       }
     },
+    "node_modules/temporal-polyfill": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/temporal-polyfill/-/temporal-polyfill-0.2.3.tgz",
+      "integrity": "sha512-7ZJRc7wq/1XjrOQYkkNpgo2qfE9XLrUU8D/DS+LAC/T0bYqZ46rW6dow0sOTXTPZS4bwer8bD/0OyuKQBfA3yw==",
+      "dependencies": {
+        "temporal-spec": "^0.2.0"
+      }
+    },
+    "node_modules/temporal-spec": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/temporal-spec/-/temporal-spec-0.2.0.tgz",
+      "integrity": "sha512-r1AT0XdEp8TMQ13FLvOt8mOtAxDQsRt2QU5rSWCA7YfshddU651Y1NHVrceLANvixKdf9fYO8B/S9fXHodB7HQ=="
+    },
     "node_modules/terser": {
-      "version": "5.27.0",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
-      "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
+      "version": "5.30.3",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz",
+      "integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==",
       "dependencies": {
         "@jridgewell/source-map": "^0.3.3",
         "acorn": "^8.8.2",
@@ -10287,6 +11497,25 @@
       "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
       "dev": true
     },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/throttle-debounce": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
@@ -10307,18 +11536,18 @@
       "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
     },
     "node_modules/tinypool": {
-      "version": "0.8.2",
-      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz",
-      "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==",
+      "version": "0.8.3",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.3.tgz",
+      "integrity": "sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==",
       "dev": true,
       "engines": {
         "node": ">=14.0.0"
       }
     },
     "node_modules/tinyspy": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz",
-      "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==",
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
+      "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
       "dev": true,
       "engines": {
         "node": ">=14.0.0"
@@ -10348,41 +11577,10 @@
       "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz",
       "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ=="
     },
-    "node_modules/tough-cookie": {
-      "version": "4.1.3",
-      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
-      "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
-      "dev": true,
-      "dependencies": {
-        "psl": "^1.1.33",
-        "punycode": "^2.1.1",
-        "universalify": "^0.2.0",
-        "url-parse": "^1.5.3"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/tough-cookie/node_modules/universalify": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
-      "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
-      "dev": true,
-      "engines": {
-        "node": ">= 4.0.0"
-      }
-    },
     "node_modules/tr46": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
-      "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
-      "dev": true,
-      "dependencies": {
-        "punycode": "^2.3.1"
-      },
-      "engines": {
-        "node": ">=18"
-      }
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
     },
     "node_modules/tributejs": {
       "version": "5.1.3",
@@ -10390,9 +11588,9 @@
       "integrity": "sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ=="
     },
     "node_modules/ts-api-utils": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz",
-      "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+      "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
       "dev": true,
       "engines": {
         "node": ">=16"
@@ -10409,6 +11607,35 @@
         "node": ">=6.10"
       }
     },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
+    },
+    "node_modules/tsconfig-paths": {
+      "version": "3.15.0",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+      "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+      "dev": true,
+      "dependencies": {
+        "@types/json5": "^0.0.29",
+        "json5": "^1.0.2",
+        "minimist": "^1.2.6",
+        "strip-bom": "^3.0.0"
+      }
+    },
+    "node_modules/tsconfig-paths/node_modules/json5": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+      "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+      "dev": true,
+      "dependencies": {
+        "minimist": "^1.2.0"
+      },
+      "bin": {
+        "json5": "lib/cli.js"
+      }
+    },
     "node_modules/tslib": {
       "version": "2.6.2",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
@@ -10449,29 +11676,30 @@
       }
     },
     "node_modules/typed-array-buffer": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
-      "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
+      "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.1",
-        "is-typed-array": "^1.1.10"
+        "call-bind": "^1.0.7",
+        "es-errors": "^1.3.0",
+        "is-typed-array": "^1.1.13"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/typed-array-byte-length": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
-      "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz",
+      "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
+        "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
-        "has-proto": "^1.0.1",
-        "is-typed-array": "^1.1.10"
+        "gopd": "^1.0.1",
+        "has-proto": "^1.0.3",
+        "is-typed-array": "^1.1.13"
       },
       "engines": {
         "node": ">= 0.4"
@@ -10481,16 +11709,17 @@
       }
     },
     "node_modules/typed-array-byte-offset": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
-      "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz",
+      "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==",
       "dev": true,
       "dependencies": {
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.2",
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
-        "has-proto": "^1.0.1",
-        "is-typed-array": "^1.1.10"
+        "gopd": "^1.0.1",
+        "has-proto": "^1.0.3",
+        "is-typed-array": "^1.1.13"
       },
       "engines": {
         "node": ">= 0.4"
@@ -10500,23 +11729,29 @@
       }
     },
     "node_modules/typed-array-length": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
-      "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz",
+      "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
+        "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
-        "is-typed-array": "^1.1.9"
+        "gopd": "^1.0.1",
+        "has-proto": "^1.0.3",
+        "is-typed-array": "^1.1.13",
+        "possible-typed-array-names": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/typescript": {
-      "version": "5.3.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
-      "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+      "version": "5.4.4",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
+      "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
       "devOptional": true,
       "peer": true,
       "bin": {
@@ -10528,20 +11763,20 @@
       }
     },
     "node_modules/typo-js": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.3.tgz",
-      "integrity": "sha512-67Hyl94beZX8gmTap7IDPrG5hy2cHftgsCAcGvE1tzuxGT+kRB+zSBin0wIMwysYw8RUCBCvv9UfQl8TNM75dA=="
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.4.tgz",
+      "integrity": "sha512-Oy/k+tFle5NAA3J/yrrYGfvEnPVrDZ8s8/WCwjUE75k331QyKIsFss7byQ/PzBmXLY6h1moRnZbnaxWBe3I3CA=="
     },
     "node_modules/uc.micro": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.0.0.tgz",
-      "integrity": "sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+      "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
       "dev": true
     },
     "node_modules/ufo": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz",
-      "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==",
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz",
+      "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==",
       "dev": true
     },
     "node_modules/uint8-to-base64": {
@@ -10620,12 +11855,12 @@
       }
     },
     "node_modules/updates": {
-      "version": "15.1.1",
-      "resolved": "https://registry.npmjs.org/updates/-/updates-15.1.1.tgz",
-      "integrity": "sha512-dMz/4251b0lV7yR58tuydCKaiWxOa18YM8fnRgtiDVzQ5ALopTZhMckv00w0nSMj6OFMFKLshTZGkX4dAebaaw==",
+      "version": "16.0.0",
+      "resolved": "https://registry.npmjs.org/updates/-/updates-16.0.0.tgz",
+      "integrity": "sha512-Ra3QUu/rfbSCsG83zNNvoRQt0FVT3qULBSALYTlwTDX6oyz7R5GQAYwqJoIG/RDjYAXpwr3usrInoyHHTP6cag==",
       "dev": true,
       "bin": {
-        "updates": "bin/updates.js"
+        "updates": "dist/updates.js"
       },
       "engines": {
         "node": ">=18"
@@ -10645,16 +11880,6 @@
       "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==",
       "dev": true
     },
-    "node_modules/url-parse": {
-      "version": "1.5.10",
-      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
-      "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
-      "dev": true,
-      "dependencies": {
-        "querystringify": "^2.1.1",
-        "requires-port": "^1.0.0"
-      }
-    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -10717,15 +11942,20 @@
         "builtins": "^1.0.3"
       }
     },
+    "node_modules/vanilla-colorful": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/vanilla-colorful/-/vanilla-colorful-0.7.2.tgz",
+      "integrity": "sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg=="
+    },
     "node_modules/vite": {
-      "version": "5.0.12",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
-      "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
+      "version": "5.2.8",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz",
+      "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==",
       "dev": true,
       "dependencies": {
-        "esbuild": "^0.19.3",
-        "postcss": "^8.4.32",
-        "rollup": "^4.2.0"
+        "esbuild": "^0.20.1",
+        "postcss": "^8.4.38",
+        "rollup": "^4.13.0"
       },
       "bin": {
         "vite": "bin/vite.js"
@@ -10773,9 +12003,9 @@
       }
     },
     "node_modules/vite-node": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz",
-      "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz",
+      "integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==",
       "dev": true,
       "dependencies": {
         "cac": "^6.7.14",
@@ -10795,9 +12025,9 @@
       }
     },
     "node_modules/vite-string-plugin": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.1.3.tgz",
-      "integrity": "sha512-uHL8BV2tBf32T2slYpS0vRzGVrAS3iuivtGknjzyecvpSq2AiBSkyLAjEvvIZuZGDDGFHyGX+5+yc3OBPjWDlA==",
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.1.5.tgz",
+      "integrity": "sha512-KRCIFX3PWVUuEjpi9O7EKLT9E27OqOA3RimIvVx6cziLAUxvnk2VvHQfMrP+mKkqyqqSmnnYyTig3OyDnK/zlA==",
       "dev": true
     },
     "node_modules/vite/node_modules/@types/estree": {
@@ -10821,9 +12051,9 @@
       }
     },
     "node_modules/vite/node_modules/rollup": {
-      "version": "4.9.6",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz",
-      "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.0.tgz",
+      "integrity": "sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==",
       "dev": true,
       "dependencies": {
         "@types/estree": "1.0.5"
@@ -10836,35 +12066,36 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.9.6",
-        "@rollup/rollup-android-arm64": "4.9.6",
-        "@rollup/rollup-darwin-arm64": "4.9.6",
-        "@rollup/rollup-darwin-x64": "4.9.6",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.9.6",
-        "@rollup/rollup-linux-arm64-gnu": "4.9.6",
-        "@rollup/rollup-linux-arm64-musl": "4.9.6",
-        "@rollup/rollup-linux-riscv64-gnu": "4.9.6",
-        "@rollup/rollup-linux-x64-gnu": "4.9.6",
-        "@rollup/rollup-linux-x64-musl": "4.9.6",
-        "@rollup/rollup-win32-arm64-msvc": "4.9.6",
-        "@rollup/rollup-win32-ia32-msvc": "4.9.6",
-        "@rollup/rollup-win32-x64-msvc": "4.9.6",
+        "@rollup/rollup-android-arm-eabi": "4.14.0",
+        "@rollup/rollup-android-arm64": "4.14.0",
+        "@rollup/rollup-darwin-arm64": "4.14.0",
+        "@rollup/rollup-darwin-x64": "4.14.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.14.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.14.0",
+        "@rollup/rollup-linux-arm64-musl": "4.14.0",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.14.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.14.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.14.0",
+        "@rollup/rollup-linux-x64-gnu": "4.14.0",
+        "@rollup/rollup-linux-x64-musl": "4.14.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.14.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.14.0",
+        "@rollup/rollup-win32-x64-msvc": "4.14.0",
         "fsevents": "~2.3.2"
       }
     },
     "node_modules/vitest": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz",
-      "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz",
+      "integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==",
       "dev": true,
       "dependencies": {
-        "@vitest/expect": "1.2.2",
-        "@vitest/runner": "1.2.2",
-        "@vitest/snapshot": "1.2.2",
-        "@vitest/spy": "1.2.2",
-        "@vitest/utils": "1.2.2",
+        "@vitest/expect": "1.4.0",
+        "@vitest/runner": "1.4.0",
+        "@vitest/snapshot": "1.4.0",
+        "@vitest/spy": "1.4.0",
+        "@vitest/utils": "1.4.0",
         "acorn-walk": "^8.3.2",
-        "cac": "^6.7.14",
         "chai": "^4.3.10",
         "debug": "^4.3.4",
         "execa": "^8.0.1",
@@ -10873,11 +12104,11 @@
         "pathe": "^1.1.1",
         "picocolors": "^1.0.0",
         "std-env": "^3.5.0",
-        "strip-literal": "^1.3.0",
+        "strip-literal": "^2.0.0",
         "tinybench": "^2.5.1",
         "tinypool": "^0.8.2",
         "vite": "^5.0.0",
-        "vite-node": "1.2.2",
+        "vite-node": "1.4.0",
         "why-is-node-running": "^2.2.2"
       },
       "bin": {
@@ -10892,8 +12123,8 @@
       "peerDependencies": {
         "@edge-runtime/vm": "*",
         "@types/node": "^18.0.0 || >=20.0.0",
-        "@vitest/browser": "^1.0.0",
-        "@vitest/ui": "^1.0.0",
+        "@vitest/browser": "1.4.0",
+        "@vitest/ui": "1.4.0",
         "happy-dom": "*",
         "jsdom": "*"
       },
@@ -10919,9 +12150,9 @@
       }
     },
     "node_modules/vitest/node_modules/magic-string": {
-      "version": "0.30.7",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
-      "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
+      "version": "0.30.9",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz",
+      "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==",
       "dev": true,
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
@@ -10931,15 +12162,15 @@
       }
     },
     "node_modules/vue": {
-      "version": "3.4.15",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz",
-      "integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==",
+      "version": "3.4.21",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz",
+      "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==",
       "dependencies": {
-        "@vue/compiler-dom": "3.4.15",
-        "@vue/compiler-sfc": "3.4.15",
-        "@vue/runtime-dom": "3.4.15",
-        "@vue/server-renderer": "3.4.15",
-        "@vue/shared": "3.4.15"
+        "@vue/compiler-dom": "3.4.21",
+        "@vue/compiler-sfc": "3.4.21",
+        "@vue/runtime-dom": "3.4.21",
+        "@vue/server-renderer": "3.4.21",
+        "@vue/shared": "3.4.21"
       },
       "peerDependencies": {
         "typescript": "*"
@@ -10959,6 +12190,15 @@
         "vue": "^3.2.37"
       }
     },
+    "node_modules/vue-chartjs": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.0.tgz",
+      "integrity": "sha512-8XqX0JU8vFZ+WA2/knz4z3ThClduni2Nm0BMe2u0mXgTfd9pXrmJ07QBI+WAij5P/aPmPMX54HCE1seWL37ZdQ==",
+      "peerDependencies": {
+        "chart.js": "^4.1.1",
+        "vue": "^3.0.0-0 || ^2.7.0"
+      }
+    },
     "node_modules/vue-eslint-parser": {
       "version": "9.4.2",
       "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz",
@@ -11016,31 +12256,10 @@
         "vue": "^3.2.29"
       }
     },
-    "node_modules/w3c-xmlserializer": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
-      "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
-      "dev": true,
-      "dependencies": {
-        "xml-name-validator": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
-      "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
-      "dev": true,
-      "engines": {
-        "node": ">=18"
-      }
-    },
     "node_modules/watchpack": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
-      "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
+      "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
       "dependencies": {
         "glob-to-regexp": "^0.4.1",
         "graceful-fs": "^4.1.2"
@@ -11064,25 +12283,25 @@
       }
     },
     "node_modules/webpack": {
-      "version": "5.90.1",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz",
-      "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==",
+      "version": "5.91.0",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz",
+      "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==",
       "dependencies": {
         "@types/eslint-scope": "^3.7.3",
         "@types/estree": "^1.0.5",
-        "@webassemblyjs/ast": "^1.11.5",
-        "@webassemblyjs/wasm-edit": "^1.11.5",
-        "@webassemblyjs/wasm-parser": "^1.11.5",
+        "@webassemblyjs/ast": "^1.12.1",
+        "@webassemblyjs/wasm-edit": "^1.12.1",
+        "@webassemblyjs/wasm-parser": "^1.12.1",
         "acorn": "^8.7.1",
         "acorn-import-assertions": "^1.9.0",
         "browserslist": "^4.21.10",
         "chrome-trace-event": "^1.0.2",
-        "enhanced-resolve": "^5.15.0",
+        "enhanced-resolve": "^5.16.0",
         "es-module-lexer": "^1.2.1",
         "eslint-scope": "5.1.1",
         "events": "^3.2.0",
         "glob-to-regexp": "^0.4.1",
-        "graceful-fs": "^4.2.9",
+        "graceful-fs": "^4.2.11",
         "json-parse-even-better-errors": "^2.3.1",
         "loader-runner": "^4.2.0",
         "mime-types": "^2.1.27",
@@ -11090,7 +12309,7 @@
         "schema-utils": "^3.2.0",
         "tapable": "^2.1.1",
         "terser-webpack-plugin": "^5.3.10",
-        "watchpack": "^2.4.0",
+        "watchpack": "^2.4.1",
         "webpack-sources": "^3.2.3"
       },
       "bin": {
@@ -11261,40 +12480,29 @@
         "node": ">=10.13.0"
       }
     },
-    "node_modules/whatwg-encoding": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
-      "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
-      "dev": true,
-      "dependencies": {
-        "iconv-lite": "0.6.3"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    },
     "node_modules/whatwg-mimetype": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
-      "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+      "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
       "dev": true,
       "engines": {
-        "node": ">=18"
+        "node": ">=12"
       }
     },
     "node_modules/whatwg-url": {
-      "version": "14.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz",
-      "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==",
-      "dev": true,
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
       "dependencies": {
-        "tr46": "^5.0.0",
-        "webidl-conversions": "^7.0.0"
-      },
-      "engines": {
-        "node": ">=18"
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
       }
     },
+    "node_modules/whatwg-url/node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+    },
     "node_modules/which": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -11325,17 +12533,61 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/which-typed-array": {
-      "version": "1.1.13",
-      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
-      "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
+    "node_modules/which-builtin-type": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz",
+      "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==",
       "dev": true,
       "dependencies": {
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.4",
+        "function.prototype.name": "^1.1.5",
+        "has-tostringtag": "^1.0.0",
+        "is-async-function": "^2.0.0",
+        "is-date-object": "^1.0.5",
+        "is-finalizationregistry": "^1.0.2",
+        "is-generator-function": "^1.0.10",
+        "is-regex": "^1.1.4",
+        "is-weakref": "^1.0.2",
+        "isarray": "^2.0.5",
+        "which-boxed-primitive": "^1.0.2",
+        "which-collection": "^1.0.1",
+        "which-typed-array": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/which-collection": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+      "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+      "dev": true,
+      "dependencies": {
+        "is-map": "^2.0.3",
+        "is-set": "^2.0.3",
+        "is-weakmap": "^2.0.2",
+        "is-weakset": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/which-typed-array": {
+      "version": "1.1.15",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
+      "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
+      "dev": true,
+      "dependencies": {
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
         "gopd": "^1.0.1",
-        "has-tostringtag": "^1.0.0"
+        "has-tostringtag": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -11386,7 +12638,6 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
       "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^4.0.0",
         "string-width": "^4.1.0",
@@ -11474,27 +12725,6 @@
         "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
       }
     },
-    "node_modules/ws": {
-      "version": "8.16.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
-      "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=10.0.0"
-      },
-      "peerDependencies": {
-        "bufferutil": "^4.0.1",
-        "utf-8-validate": ">=5.0.2"
-      },
-      "peerDependenciesMeta": {
-        "bufferutil": {
-          "optional": true
-        },
-        "utf-8-validate": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/xml-name-validator": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
@@ -11504,12 +12734,6 @@
         "node": ">=12"
       }
     },
-    "node_modules/xmlchars": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
-      "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
-      "dev": true
-    },
     "node_modules/y18n": {
       "version": "5.0.8",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -11524,19 +12748,30 @@
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
     },
+    "node_modules/yaml": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
+      "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/yargs": {
-      "version": "17.3.1",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
-      "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
       "dev": true,
       "dependencies": {
-        "cliui": "^7.0.2",
+        "cliui": "^8.0.1",
         "escalade": "^3.1.1",
         "get-caller-file": "^2.0.5",
         "require-directory": "^2.1.1",
         "string-width": "^4.2.3",
         "y18n": "^5.0.5",
-        "yargs-parser": "^21.0.0"
+        "yargs-parser": "^21.1.1"
       },
       "engines": {
         "node": ">=12"
@@ -11551,6 +12786,37 @@
         "node": ">=12"
       }
     },
+    "node_modules/yargs/node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dev": true,
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs/node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
     "node_modules/yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 569955d815..f58c3b4d8f 100644
--- a/package.json
+++ b/package.json
@@ -4,89 +4,99 @@
     "node": ">= 18.0.0"
   },
   "dependencies": {
-    "@citation-js/core": "0.7.6",
-    "@citation-js/plugin-bibtex": "0.7.8",
-    "@citation-js/plugin-csl": "0.7.6",
+    "@citation-js/core": "0.7.9",
+    "@citation-js/plugin-bibtex": "0.7.9",
+    "@citation-js/plugin-csl": "0.7.9",
     "@citation-js/plugin-software-formats": "0.6.1",
-    "@claviska/jquery-minicolors": "2.3.6",
-    "@github/markdown-toolbar-element": "2.2.1",
-    "@github/relative-time-element": "4.3.1",
+    "@github/markdown-toolbar-element": "2.2.3",
+    "@github/relative-time-element": "4.4.0",
     "@github/text-expander-element": "2.6.1",
     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
-    "@primer/octicons": "19.8.0",
-    "@webcomponents/custom-elements": "1.6.0",
+    "@primer/octicons": "19.9.0",
     "add-asset-webpack-plugin": "2.0.1",
     "ansi_up": "6.0.2",
-    "asciinema-player": "3.6.3",
-    "clippie": "4.0.6",
-    "css-loader": "6.10.0",
+    "asciinema-player": "3.7.1",
+    "chart.js": "4.4.2",
+    "chartjs-adapter-dayjs-4": "1.0.4",
+    "chartjs-plugin-zoom": "2.0.1",
+    "clippie": "4.0.7",
+    "css-loader": "7.0.0",
+    "dayjs": "1.11.10",
     "dropzone": "6.0.0-beta.2",
     "easymde": "2.18.0",
-    "esbuild-loader": "4.0.3",
+    "esbuild-loader": "4.1.0",
     "escape-goat": "4.0.0",
     "fast-glob": "3.3.2",
-    "htmx.org": "1.9.10",
+    "htmx.org": "1.9.11",
+    "idiomorph": "0.3.0",
     "jquery": "3.7.1",
-    "katex": "0.16.9",
+    "katex": "0.16.10",
     "license-checker-webpack-plugin": "0.2.1",
-    "mermaid": "10.7.0",
-    "mini-css-extract-plugin": "2.8.0",
-    "minimatch": "9.0.3",
-    "monaco-editor": "0.45.0",
+    "mermaid": "10.9.0",
+    "mini-css-extract-plugin": "2.8.1",
+    "minimatch": "9.0.4",
+    "monaco-editor": "0.47.0",
     "monaco-editor-webpack-plugin": "7.1.0",
-    "pdfobject": "2.2.12",
+    "pdfobject": "2.3.0",
+    "postcss": "8.4.38",
+    "postcss-loader": "8.1.1",
+    "postcss-nesting": "12.1.1",
     "pretty-ms": "9.0.0",
     "sortablejs": "1.15.2",
-    "swagger-ui-dist": "5.11.2",
+    "swagger-ui-dist": "5.13.0",
+    "tailwindcss": "3.4.3",
+    "temporal-polyfill": "0.2.3",
     "throttle-debounce": "5.0.0",
     "tinycolor2": "1.6.0",
     "tippy.js": "6.3.7",
     "toastify-js": "1.12.0",
     "tributejs": "5.1.3",
     "uint8-to-base64": "0.2.0",
-    "vue": "3.4.15",
+    "vanilla-colorful": "0.7.2",
+    "vue": "3.4.21",
     "vue-bar-graph": "2.0.0",
+    "vue-chartjs": "5.3.0",
     "vue-loader": "17.4.2",
     "vue3-calendar-heatmap": "2.0.5",
-    "webpack": "5.90.1",
+    "webpack": "5.91.0",
     "webpack-cli": "5.1.4",
     "wrap-ansi": "9.0.0"
   },
   "devDependencies": {
     "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
-    "@playwright/test": "1.41.1",
-    "@stoplight/spectral-cli": "6.11.0",
-    "@stylistic/eslint-plugin-js": "1.5.4",
-    "@stylistic/stylelint-plugin": "2.0.0",
-    "@vitejs/plugin-vue": "5.0.3",
-    "eslint": "8.56.0",
+    "@playwright/test": "1.42.1",
+    "@stoplight/spectral-cli": "6.11.1",
+    "@stylistic/eslint-plugin-js": "1.7.0",
+    "@stylistic/stylelint-plugin": "2.1.1",
+    "@vitejs/plugin-vue": "5.0.4",
+    "eslint": "8.57.0",
     "eslint-plugin-array-func": "4.0.0",
+    "eslint-plugin-github": "4.10.2",
     "eslint-plugin-i": "2.29.1",
     "eslint-plugin-jquery": "1.5.1",
     "eslint-plugin-no-jquery": "2.7.0",
     "eslint-plugin-no-use-extend-native": "0.5.0",
-    "eslint-plugin-regexp": "2.2.0",
-    "eslint-plugin-sonarjs": "0.23.0",
-    "eslint-plugin-unicorn": "50.0.1",
-    "eslint-plugin-vitest": "0.3.21",
-    "eslint-plugin-vitest-globals": "1.4.0",
-    "eslint-plugin-vue": "9.21.1",
-    "eslint-plugin-vue-scoped-css": "2.7.2",
+    "eslint-plugin-regexp": "2.4.0",
+    "eslint-plugin-sonarjs": "0.25.1",
+    "eslint-plugin-unicorn": "52.0.0",
+    "eslint-plugin-vitest": "0.4.1",
+    "eslint-plugin-vitest-globals": "1.5.0",
+    "eslint-plugin-vue": "9.24.0",
+    "eslint-plugin-vue-scoped-css": "2.8.0",
     "eslint-plugin-wc": "2.0.4",
-    "jsdom": "24.0.0",
+    "happy-dom": "14.5.0",
     "markdownlint-cli": "0.39.0",
     "postcss-html": "1.6.0",
-    "stylelint": "16.2.1",
+    "stylelint": "16.3.1",
     "stylelint-declaration-block-no-ignored-properties": "2.8.0",
     "stylelint-declaration-strict-value": "1.10.4",
+    "stylelint-value-no-unknown-custom-properties": "6.0.1",
     "svgo": "3.2.0",
-    "updates": "15.1.1",
-    "vite-string-plugin": "1.1.3",
-    "vitest": "1.2.2"
+    "updates": "16.0.0",
+    "vite-string-plugin": "1.1.5",
+    "vitest": "1.4.0"
   },
   "browserslist": [
-    "defaults",
-    "not ie > 0",
-    "not ie_mob > 0"
+    "defaults"
   ]
 }
diff --git a/playwright.config.js b/playwright.config.js
index b7badf1cc0..bdd303ae25 100644
--- a/playwright.config.js
+++ b/playwright.config.js
@@ -20,7 +20,7 @@ export default {
      * Maximum time expect() should wait for the condition to be met.
      * For example in `await expect(locator).toHaveText();`
      */
-    timeout: 2000
+    timeout: 2000,
   },
 
   /* Fail the build on CI if you accidentally left test.only in the source code. */
diff --git a/poetry.lock b/poetry.lock
index 74d202c919..951a0fa7a8 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
 
 [[package]]
 name = "click"
@@ -27,12 +27,12 @@ files = [
 
 [[package]]
 name = "cssbeautifier"
-version = "1.14.11"
+version = "1.15.1"
 description = "CSS unobfuscator and beautifier."
 optional = false
 python-versions = "*"
 files = [
-    {file = "cssbeautifier-1.14.11.tar.gz", hash = "sha256:40544c2b62bbcb64caa5e7f37a02df95654e5ce1bcacadac4ca1f3dc89c31513"},
+    {file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"},
 ]
 
 [package.dependencies]
@@ -67,13 +67,12 @@ tqdm = ">=4.62.2,<5.0.0"
 
 [[package]]
 name = "editorconfig"
-version = "0.12.3"
+version = "0.12.4"
 description = "EditorConfig File Locator and Interpreter for Python"
 optional = false
 python-versions = "*"
 files = [
-    {file = "EditorConfig-0.12.3-py3-none-any.whl", hash = "sha256:6b0851425aa875b08b16789ee0eeadbd4ab59666e9ebe728e526314c4a2e52c1"},
-    {file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"},
+    {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
 ]
 
 [[package]]
@@ -100,12 +99,12 @@ files = [
 
 [[package]]
 name = "jsbeautifier"
-version = "1.14.11"
+version = "1.15.1"
 description = "JavaScript unobfuscator and beautifier."
 optional = false
 python-versions = "*"
 files = [
-    {file = "jsbeautifier-1.14.11.tar.gz", hash = "sha256:6b632581ea60dd1c133cd25a48ad187b4b91f526623c4b0fb5443ef805250505"},
+    {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"},
 ]
 
 [package.dependencies]
@@ -114,18 +113,15 @@ six = ">=1.13.0"
 
 [[package]]
 name = "json5"
-version = "0.9.14"
+version = "0.9.24"
 description = "A Python implementation of the JSON5 data format."
 optional = false
-python-versions = "*"
+python-versions = ">=3.8"
 files = [
-    {file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"},
-    {file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"},
+    {file = "json5-0.9.24-py3-none-any.whl", hash = "sha256:4ca101fd5c7cb47960c055ef8f4d0e31e15a7c6c48c3b6f1473fc83b6c462a13"},
+    {file = "json5-0.9.24.tar.gz", hash = "sha256:0c638399421da959a20952782800e5c1a78c14e08e1dc9738fa10d8ec14d58c8"},
 ]
 
-[package.extras]
-dev = ["hypothesis"]
-
 [[package]]
 name = "pathspec"
 version = "0.12.1"
@@ -322,13 +318,13 @@ files = [
 
 [[package]]
 name = "tqdm"
-version = "4.66.1"
+version = "4.66.2"
 description = "Fast, Extensible Progress Meter"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"},
-    {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"},
+    {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
+    {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
 ]
 
 [package.dependencies]
@@ -342,13 +338,13 @@ telegram = ["requests"]
 
 [[package]]
 name = "yamllint"
-version = "1.33.0"
+version = "1.35.1"
 description = "A linter for YAML files."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "yamllint-1.33.0-py3-none-any.whl", hash = "sha256:28a19f5d68d28d8fec538a1db21bb2d84c7dc2e2ea36266da8d4d1c5a683814d"},
-    {file = "yamllint-1.33.0.tar.gz", hash = "sha256:2dceab9ef2d99518a2fcf4ffc964d44250ac4459be1ba3ca315118e4a1a81f7d"},
+    {file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"},
+    {file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"},
 ]
 
 [package.dependencies]
@@ -360,5 +356,5 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
 
 [metadata]
 lock-version = "2.0"
-python-versions = "^3.8"
-content-hash = "175c87d138a47ba190a2c3f16b801f694915cc6f2367a358585df9cd1b17ff96"
+python-versions = "^3.10"
+content-hash = "cd2ff218e9f27a464dfbc8ec2387824a90f4360e04c3f2e58cc375796b7df33a"
diff --git a/public/assets/img/favicon.svg b/public/assets/img/favicon.svg
index afeeacb77c..43291345df 100644
--- a/public/assets/img/favicon.svg
+++ b/public/assets/img/favicon.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640" xml:space="preserve" width="32" height="32"><path style="fill:#fff" d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z"/><path style="fill:#609926" d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z"/><path style="fill:#609926" d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/logo.svg b/public/assets/img/logo.svg
index afeeacb77c..43291345df 100644
--- a/public/assets/img/logo.svg
+++ b/public/assets/img/logo.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640" xml:space="preserve" width="32" height="32"><path style="fill:#fff" d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z"/><path style="fill:#609926" d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z"/><path style="fill:#609926" d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-bitbucket.svg b/public/assets/img/svg/gitea-bitbucket.svg
index b900335ea1..83e4c5c6e7 100644
--- a/public/assets/img/svg/gitea-bitbucket.svg
+++ b/public/assets/img/svg/gitea-bitbucket.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-bitbucket__svg gitea-bitbucket__gitea-bitbucket svg gitea-bitbucket" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 295" width="16" height="16"><g fill="#205081"><path d="M128 0C57.732 0 .012 18.822.012 42.663c0 6.274 15.057 95.364 21.331 130.498 2.51 16.312 43.918 38.898 106.657 38.898 62.74 0 102.893-22.586 106.657-38.898 6.274-35.134 21.331-124.224 21.331-130.498C254.734 18.822 198.268 0 128 0m0 183.199c-22.586 0-40.153-17.567-40.153-40.153s17.567-40.153 40.153-40.153 40.153 17.567 40.153 40.153c0 21.331-17.567 40.153-40.153 40.153m0-127.988c-45.172 0-81.561-7.53-81.561-17.567 0-10.039 36.389-17.567 81.561-17.567s81.561 7.528 81.561 17.567c0 10.038-36.389 17.567-81.561 17.567"/><path d="M220.608 207.04c-2.51 0-3.764 1.255-3.764 1.255s-31.37 25.096-87.835 25.096c-56.466 0-87.835-25.096-87.835-25.096s-2.51-1.255-3.765-1.255c-2.51 0-5.019 1.255-5.019 5.02v1.254c5.02 26.35 8.784 45.172 8.784 47.682 3.764 18.822 41.408 33.88 86.58 33.88s82.816-15.058 86.58-33.88c0-2.51 3.765-21.332 8.784-47.682v-1.255c1.255-2.51 0-5.019-2.51-5.019"/><circle cx="128" cy="141.791" r="20.077"/></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.42 62.42" class="svg gitea-bitbucket" width="16" height="16" aria-hidden="true"><defs><linearGradient id="gitea-bitbucket__a" x1="64.01" x2="32.99" y1="30.27" y2="54.48" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient></defs><g data-name="Layer 2"><path fill="#2684ff" d="M2 3.13a2 2 0 0 0-2 2.32l8.49 51.54a2.72 2.72 0 0 0 2.66 2.27h40.73a2 2 0 0 0 2-1.68l8.49-52.12a2 2 0 0 0-2-2.32Zm35.75 37.25h-13l-3.52-18.39H40.9Z"/><path fill="url(#gitea-bitbucket__a)" d="M59.67 25.12H40.9l-3.15 18.39h-13L9.4 61.73a2.7 2.7 0 0 0 1.75.66h40.74a2 2 0 0 0 2-1.68Z" transform="translate(0 -3.13)"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-discord.svg b/public/assets/img/svg/gitea-discord.svg
index 6ebbdcdcc3..2edcb4fed7 100644
--- a/public/assets/img/svg/gitea-discord.svg
+++ b/public/assets/img/svg/gitea-discord.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-discord__svg gitea-discord__gitea-discord svg gitea-discord" preserveAspectRatio="xMidYMid" viewBox="0 0 256 293" width="16" height="16"><path fill="#7289DA" d="M226.011 0H29.99C13.459 0 0 13.458 0 30.135v197.778c0 16.677 13.458 30.135 29.989 30.135h165.888l-7.754-27.063 18.725 17.408 17.7 16.384L256 292.571V30.135C256 13.458 242.542 0 226.011 0m-56.466 191.05s-5.266-6.291-9.655-11.85c19.164-5.413 26.478-17.408 26.478-17.408-5.998 3.95-11.703 6.73-16.823 8.63-7.314 3.073-14.336 5.12-21.211 6.291-14.044 2.633-26.917 1.902-37.888-.146-8.339-1.61-15.507-3.95-21.504-6.29-3.365-1.317-7.022-2.926-10.68-4.974-.438-.293-.877-.439-1.316-.732a2 2 0 0 1-.585-.438c-2.633-1.463-4.096-2.487-4.096-2.487s7.022 11.703 25.6 17.261c-4.388 5.56-9.801 12.142-9.801 12.142-32.33-1.024-44.617-22.235-44.617-22.235 0-47.104 21.065-85.285 21.065-85.285 21.065-15.799 41.106-15.36 41.106-15.36l1.463 1.756C80.75 77.53 68.608 89.088 68.608 89.088s3.218-1.755 8.63-4.242c15.653-6.876 28.088-8.777 33.208-9.216.877-.147 1.609-.293 2.487-.293a123.8 123.8 0 0 1 29.55-.292c13.896 1.609 28.818 5.705 44.031 14.043 0 0-11.556-10.971-36.425-18.578l2.048-2.34s20.041-.44 41.106 15.36c0 0 21.066 38.18 21.066 85.284 0 0-12.435 21.211-44.764 22.235zm-68.023-68.316c-8.338 0-14.92 7.314-14.92 16.237 0 8.924 6.728 16.238 14.92 16.238 8.339 0 14.921-7.314 14.921-16.238.147-8.923-6.582-16.237-14.92-16.237m53.394 0c-8.339 0-14.922 7.314-14.922 16.237 0 8.924 6.73 16.238 14.922 16.238 8.338 0 14.92-7.314 14.92-16.238s-6.582-16.237-14.92-16.237"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36" class="svg gitea-discord" width="16" height="16" aria-hidden="true"><path fill="#5865f2" d="M107.7 8.07A105.2 105.2 0 0 0 81.47 0a72 72 0 0 0-3.36 6.83 97.7 97.7 0 0 0-29.11 0A72 72 0 0 0 45.64 0a106 106 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.7 105.7 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.4 68.4 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.7 68.7 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.3 105.3 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15M42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69m42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-facebook.svg b/public/assets/img/svg/gitea-facebook.svg
index cbeb76b127..6101becad2 100644
--- a/public/assets/img/svg/gitea-facebook.svg
+++ b/public/assets/img/svg/gitea-facebook.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-facebook__svg gitea-facebook__gitea-facebook svg gitea-facebook" style="shape-rendering:geometricPrecision;text-rendering:geometricPrecision;image-rendering:optimizeQuality;fill-rule:evenodd;clip-rule:evenodd" viewBox="0 0 128 128" width="16" height="16"><path fill="#395b97" d="M93.5 8.5q-2.177 1.203-5 1.5L10 88.5q-.297 2.823-1.5 5a552 552 0 0 1-.5-56Q11.75 11.75 37.5 8a552 552 0 0 1 56 .5" style="opacity:.995"/><path fill="#366098" d="M93.5 8.5q23.832 6.337 26 31a677 677 0 0 0-1.5 37l-35 35a32.4 32.4 0 0 0-.5 8 442 442 0 0 1-1-42h14a380 380 0 0 0 3-17h-17q-3.75-20.745 17-18v-16q-38.632-4.865-33 34h-14v17h14v42q-14.01.25-28-.5-23.177-2.93-29-25.5 1.203-2.177 1.5-5L88.5 10q2.823-.297 5-1.5" style="opacity:.976"/><path fill="#346499" d="M119.5 39.5q.25 25.005-.5 50-5.432 30.368-36.5 30a32.4 32.4 0 0 1 .5-8l35-35q.254-18.76 1.5-37" style="opacity:.918"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" clip-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="0 0 14222 14222" class="svg gitea-facebook" width="16" height="16" aria-hidden="true"><g fill-rule="nonzero"><path fill="#1977f3" d="M14222 7111C14222 3184 11038 0 7111 0S0 3184 0 7111c0 3549 2600 6491 6000 7025V9167H4194V7111h1806V5544c0-1782 1062-2767 2686-2767 778 0 1592 139 1592 139v1750h-897c-883 0-1159 548-1159 1111v1334h1972l-315 2056H8222v4969c3400-533 6000-3475 6000-7025"/><path fill="#fefefe" d="m9879 9167 315-2056H8222V5777c0-562 275-1111 1159-1111h897V2916s-814-139-1592-139c-1624 0-2686 984-2686 2767v1567H4194v2056h1806v4969c362 57 733 86 1111 86s749-30 1111-86V9167z"/></g></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-jetbrains.svg b/public/assets/img/svg/gitea-jetbrains.svg
new file mode 100644
index 0000000000..5821736225
--- /dev/null
+++ b/public/assets/img/svg/gitea-jetbrains.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 70 70" class="svg gitea-jetbrains" width="16" height="16" aria-hidden="true"><linearGradient id="gitea-jetbrains__a" x1=".79" x2="33.317" y1="40.089" y2="40.089" gradientUnits="userSpaceOnUse"><stop offset=".258" style="stop-color:#f97a12"/><stop offset=".459" style="stop-color:#b07b58"/><stop offset=".724" style="stop-color:#577bae"/><stop offset=".91" style="stop-color:#1e7ce5"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M17.7 54.6.8 41.2l8.4-15.6L33.3 35z" style="fill:url(#gitea-jetbrains__a)"/><linearGradient id="gitea-jetbrains__b" x1="25.767" x2="79.424" y1="24.88" y2="54.57" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#f97a12"/><stop offset=".072" style="stop-color:#cb7a3e"/><stop offset=".154" style="stop-color:#9e7b6a"/><stop offset=".242" style="stop-color:#757b91"/><stop offset=".334" style="stop-color:#537bb1"/><stop offset=".432" style="stop-color:#387ccc"/><stop offset=".538" style="stop-color:#237ce0"/><stop offset=".655" style="stop-color:#147cef"/><stop offset=".792" style="stop-color:#0b7cf7"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="m70 18.7-1.3 40.5L41.8 70 25.6 59.6 49.3 35 38.9 12.3l9.3-11.2z" style="fill:url(#gitea-jetbrains__b)"/><linearGradient id="gitea-jetbrains__c" x1="63.228" x2="48.29" y1="42.915" y2="-1.719" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".078" style="stop-color:#cb417e"/><stop offset=".16" style="stop-color:#9e4e9b"/><stop offset=".247" style="stop-color:#755bb4"/><stop offset=".339" style="stop-color:#5365ca"/><stop offset=".436" style="stop-color:#386ddb"/><stop offset=".541" style="stop-color:#2374e9"/><stop offset=".658" style="stop-color:#1478f3"/><stop offset=".794" style="stop-color:#0b7bf8"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M70 18.7 48.7 43.9l-9.8-31.6 9.3-11.2z" style="fill:url(#gitea-jetbrains__c)"/><linearGradient id="gitea-jetbrains__d" x1="10.72" x2="55.524" y1="16.473" y2="90.58" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".04" style="stop-color:#f63462"/><stop offset=".104" style="stop-color:#df3a71"/><stop offset=".167" style="stop-color:#c24383"/><stop offset=".291" style="stop-color:#ad4a91"/><stop offset=".55" style="stop-color:#755bb4"/><stop offset=".917" style="stop-color:#1d76ed"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M33.7 58.1 5.6 68.3l4.5-15.8L16 33.1 0 27.7 10.1 0l22 2.7 21.6 24.7z" style="fill:url(#gitea-jetbrains__d)"/><path d="M13.7 13.5h43.2v43.2H13.7z" style="fill:#000"/><path d="M17.7 48.6h16.2v2.7H17.7zM29.4 22.4v-3.3h-9v3.3H23v11.3h-2.6V37h9v-3.3h-2.5V22.4zM38 37.3c-1.4 0-2.6-.3-3.5-.8s-1.7-1.2-2.3-1.9l2.5-2.8c.5.6 1 1 1.5 1.3s1.1.5 1.7.5c.7 0 1.3-.2 1.8-.7.4-.5.6-1.2.6-2.3V19.1h4v11.7c0 1.1-.1 2-.4 2.8s-.7 1.4-1.3 2c-.5.5-1.2 1-2 1.2-.8.3-1.6.5-2.6.5" style="fill:#fff"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-microsoftonline.svg b/public/assets/img/svg/gitea-microsoftonline.svg
index ce4f1a5c8f..f2ce13ac22 100644
--- a/public/assets/img/svg/gitea-microsoftonline.svg
+++ b/public/assets/img/svg/gitea-microsoftonline.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-microsoftonline__svg gitea-microsoftonline__gitea-microsoftonline svg gitea-microsoftonline" viewBox="0 0 2075 2499.8" width="16" height="16"><path fill="#eb3c00" d="M0 2016.6V496.8L1344.4 0 2075 233.7v2045.9l-730.6 220.3zl1344.4 161.8V409.2L467.6 613.8v1198.3z"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48" class="svg gitea-microsoftonline" width="16" height="16" aria-hidden="true"><path fill="url(#gitea-microsoftonline__a)" d="m20.084 3.026-.224.136a8 8 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258q.111-.068.224-.131Z"/><path fill="url(#gitea-microsoftonline__b)" d="m20.084 3.026-.224.136a8 8 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258q.111-.068.224-.131Z"/><path fill="url(#gitea-microsoftonline__c)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26z"/><path fill="url(#gitea-microsoftonline__d)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26z"/><path fill="url(#gitea-microsoftonline__e)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31q.004-.132.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#gitea-microsoftonline__f)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31q.004-.132.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#gitea-microsoftonline__g)" d="M4.004 30.998"/><path fill="url(#gitea-microsoftonline__h)" d="M4.004 30.998"/><defs><radialGradient id="gitea-microsoftonline__a" cx="0" cy="0" r="1" gradientTransform="rotate(110.528 5.021 11.358)scale(33.3657 58.1966)" gradientUnits="userSpaceOnUse"><stop offset=".064" stop-color="#AE7FE2"/><stop offset="1" stop-color="#0078D4"/></radialGradient><radialGradient id="gitea-microsoftonline__c" cx="0" cy="0" r="1" gradientTransform="matrix(30.7198 -4.51832 2.98465 20.29248 10.43 36.351)" gradientUnits="userSpaceOnUse"><stop offset=".134" stop-color="#D59DFF"/><stop offset="1" stop-color="#5E438F"/></radialGradient><radialGradient id="gitea-microsoftonline__e" cx="0" cy="0" r="1" gradientTransform="matrix(-24.1583 -6.12555 10.3118 -40.66824 41.055 26.504)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><radialGradient id="gitea-microsoftonline__g" cx="0" cy="0" r="1" gradientTransform="matrix(-24.1583 -6.12555 10.3118 -40.66824 41.055 26.504)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><linearGradient id="gitea-microsoftonline__b" x1="17.512" x2="12.751" y1="37.868" y2="29.635" gradientUnits="userSpaceOnUse"><stop stop-color="#114A8B"/><stop offset="1" stop-color="#0078D4" stop-opacity="0"/></linearGradient><linearGradient id="gitea-microsoftonline__d" x1="40.357" x2="35.255" y1="25.377" y2="32.692" gradientUnits="userSpaceOnUse"><stop stop-color="#493474"/><stop offset="1" stop-color="#8C66BA" stop-opacity="0"/></linearGradient><linearGradient id="gitea-microsoftonline__f" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient><linearGradient id="gitea-microsoftonline__h" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient></defs></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-twitter.svg b/public/assets/img/svg/gitea-twitter.svg
index 5d11c6eaec..5ed1e264ca 100644
--- a/public/assets/img/svg/gitea-twitter.svg
+++ b/public/assets/img/svg/gitea-twitter.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" aria-hidden="true" class="gitea-twitter__svg gitea-twitter__gitea-twitter svg gitea-twitter" clip-rule="evenodd" viewBox="-89.009 -46.884 643.937 446.884" width="16" height="16"><path fill="#1da1f2" fill-rule="nonzero" d="M154.729 400c185.669 0 287.205-153.876 287.205-287.312 0-4.37-.089-8.72-.286-13.052A205.3 205.3 0 0 0 492 47.346c-18.087 8.044-37.55 13.458-57.968 15.899 20.841-12.501 36.84-32.278 44.389-55.852a202.4 202.4 0 0 1-64.098 24.511C395.903 12.276 369.679 0 340.641 0c-55.744 0-100.948 45.222-100.948 100.965 0 7.925.887 15.631 2.619 23.025-83.895-4.223-158.287-44.405-208.074-105.504A100.74 100.74 0 0 0 20.57 69.24c0 35.034 17.82 65.961 44.92 84.055a100.2 100.2 0 0 1-45.716-12.63c-.015.424-.015.837-.015 1.29 0 48.903 34.794 89.734 80.982 98.986a101 101 0 0 1-26.617 3.553c-6.493 0-12.821-.639-18.971-1.82 12.851 40.122 50.115 69.319 94.296 70.135-34.549 27.089-78.07 43.224-125.371 43.224A205 205 0 0 1 0 354.634c44.674 28.645 97.72 45.359 154.734 45.359"/></svg>
\ No newline at end of file
+<svg viewBox="0 0 24 24" class="svg gitea-twitter" xmlns="http://www.w3.org/2000/svg" width="16" height="16" aria-hidden="true"><path d="M14.095 10.316 22.286 1h-1.94L13.23 9.088 7.551 1H1l8.59 12.231L1 23h1.94l7.51-8.543 6 8.543H23zm-2.658 3.022-.872-1.218L3.64 2.432h2.98l5.59 7.821.869 1.219 7.265 10.166h-2.982z"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-vscodium.svg b/public/assets/img/svg/gitea-vscodium.svg
new file mode 100644
index 0000000000..6aad3d3a64
--- /dev/null
+++ b/public/assets/img/svg/gitea-vscodium.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 16 16" class="svg gitea-vscodium" width="16" height="16" aria-hidden="true"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9l.8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3s0 2.5-.2 3.7c0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8s.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg>
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index d999a1476c..bb768d5cb1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,11 +5,11 @@ description = ""
 authors = []
 
 [tool.poetry.dependencies]
-python = "^3.8"
+python = "^3.10"
 
 [tool.poetry.group.dev.dependencies]
 djlint = "1.34.1"
-yamllint = "1.33.0"
+yamllint = "1.35.1"
 
 [tool.djlint]
 profile="golang"
diff --git a/routers/api/actions/artifact.pb.go b/routers/api/actions/artifact.pb.go
new file mode 100644
index 0000000000..590eda9fb9
--- /dev/null
+++ b/routers/api/actions/artifact.pb.go
@@ -0,0 +1,1058 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.32.0
+// 	protoc        v4.25.2
+// source: artifact.proto
+
+package actions
+
+import (
+	reflect "reflect"
+	sync "sync"
+
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type CreateArtifactRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WorkflowRunBackendId    string                 `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+	WorkflowJobRunBackendId string                 `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+	Name                    string                 `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+	ExpiresAt               *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
+	Version                 int32                  `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"`
+}
+
+func (x *CreateArtifactRequest) Reset() {
+	*x = CreateArtifactRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CreateArtifactRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateArtifactRequest) ProtoMessage() {}
+
+func (x *CreateArtifactRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateArtifactRequest.ProtoReflect.Descriptor instead.
+func (*CreateArtifactRequest) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CreateArtifactRequest) GetWorkflowRunBackendId() string {
+	if x != nil {
+		return x.WorkflowRunBackendId
+	}
+	return ""
+}
+
+func (x *CreateArtifactRequest) GetWorkflowJobRunBackendId() string {
+	if x != nil {
+		return x.WorkflowJobRunBackendId
+	}
+	return ""
+}
+
+func (x *CreateArtifactRequest) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *CreateArtifactRequest) GetExpiresAt() *timestamppb.Timestamp {
+	if x != nil {
+		return x.ExpiresAt
+	}
+	return nil
+}
+
+func (x *CreateArtifactRequest) GetVersion() int32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+type CreateArtifactResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Ok              bool   `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
+	SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"`
+}
+
+func (x *CreateArtifactResponse) Reset() {
+	*x = CreateArtifactResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CreateArtifactResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateArtifactResponse) ProtoMessage() {}
+
+func (x *CreateArtifactResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateArtifactResponse.ProtoReflect.Descriptor instead.
+func (*CreateArtifactResponse) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *CreateArtifactResponse) GetOk() bool {
+	if x != nil {
+		return x.Ok
+	}
+	return false
+}
+
+func (x *CreateArtifactResponse) GetSignedUploadUrl() string {
+	if x != nil {
+		return x.SignedUploadUrl
+	}
+	return ""
+}
+
+type FinalizeArtifactRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WorkflowRunBackendId    string                  `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+	WorkflowJobRunBackendId string                  `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+	Name                    string                  `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+	Size                    int64                   `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"`
+	Hash                    *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=hash,proto3" json:"hash,omitempty"`
+}
+
+func (x *FinalizeArtifactRequest) Reset() {
+	*x = FinalizeArtifactRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *FinalizeArtifactRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FinalizeArtifactRequest) ProtoMessage() {}
+
+func (x *FinalizeArtifactRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use FinalizeArtifactRequest.ProtoReflect.Descriptor instead.
+func (*FinalizeArtifactRequest) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *FinalizeArtifactRequest) GetWorkflowRunBackendId() string {
+	if x != nil {
+		return x.WorkflowRunBackendId
+	}
+	return ""
+}
+
+func (x *FinalizeArtifactRequest) GetWorkflowJobRunBackendId() string {
+	if x != nil {
+		return x.WorkflowJobRunBackendId
+	}
+	return ""
+}
+
+func (x *FinalizeArtifactRequest) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *FinalizeArtifactRequest) GetSize() int64 {
+	if x != nil {
+		return x.Size
+	}
+	return 0
+}
+
+func (x *FinalizeArtifactRequest) GetHash() *wrapperspb.StringValue {
+	if x != nil {
+		return x.Hash
+	}
+	return nil
+}
+
+type FinalizeArtifactResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Ok         bool  `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
+	ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"`
+}
+
+func (x *FinalizeArtifactResponse) Reset() {
+	*x = FinalizeArtifactResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *FinalizeArtifactResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FinalizeArtifactResponse) ProtoMessage() {}
+
+func (x *FinalizeArtifactResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use FinalizeArtifactResponse.ProtoReflect.Descriptor instead.
+func (*FinalizeArtifactResponse) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *FinalizeArtifactResponse) GetOk() bool {
+	if x != nil {
+		return x.Ok
+	}
+	return false
+}
+
+func (x *FinalizeArtifactResponse) GetArtifactId() int64 {
+	if x != nil {
+		return x.ArtifactId
+	}
+	return 0
+}
+
+type ListArtifactsRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WorkflowRunBackendId    string                  `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+	WorkflowJobRunBackendId string                  `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+	NameFilter              *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=name_filter,json=nameFilter,proto3" json:"name_filter,omitempty"`
+	IdFilter                *wrapperspb.Int64Value  `protobuf:"bytes,4,opt,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"`
+}
+
+func (x *ListArtifactsRequest) Reset() {
+	*x = ListArtifactsRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[4]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListArtifactsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListArtifactsRequest) ProtoMessage() {}
+
+func (x *ListArtifactsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[4]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListArtifactsRequest.ProtoReflect.Descriptor instead.
+func (*ListArtifactsRequest) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *ListArtifactsRequest) GetWorkflowRunBackendId() string {
+	if x != nil {
+		return x.WorkflowRunBackendId
+	}
+	return ""
+}
+
+func (x *ListArtifactsRequest) GetWorkflowJobRunBackendId() string {
+	if x != nil {
+		return x.WorkflowJobRunBackendId
+	}
+	return ""
+}
+
+func (x *ListArtifactsRequest) GetNameFilter() *wrapperspb.StringValue {
+	if x != nil {
+		return x.NameFilter
+	}
+	return nil
+}
+
+func (x *ListArtifactsRequest) GetIdFilter() *wrapperspb.Int64Value {
+	if x != nil {
+		return x.IdFilter
+	}
+	return nil
+}
+
+type ListArtifactsResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"`
+}
+
+func (x *ListArtifactsResponse) Reset() {
+	*x = ListArtifactsResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[5]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListArtifactsResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListArtifactsResponse) ProtoMessage() {}
+
+func (x *ListArtifactsResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[5]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListArtifactsResponse.ProtoReflect.Descriptor instead.
+func (*ListArtifactsResponse) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *ListArtifactsResponse) GetArtifacts() []*ListArtifactsResponse_MonolithArtifact {
+	if x != nil {
+		return x.Artifacts
+	}
+	return nil
+}
+
+type ListArtifactsResponse_MonolithArtifact struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WorkflowRunBackendId    string                 `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+	WorkflowJobRunBackendId string                 `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+	DatabaseId              int64                  `protobuf:"varint,3,opt,name=database_id,json=databaseId,proto3" json:"database_id,omitempty"`
+	Name                    string                 `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
+	Size                    int64                  `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"`
+	CreatedAt               *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) Reset() {
+	*x = ListArtifactsResponse_MonolithArtifact{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[6]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListArtifactsResponse_MonolithArtifact) ProtoMessage() {}
+
+func (x *ListArtifactsResponse_MonolithArtifact) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[6]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListArtifactsResponse_MonolithArtifact.ProtoReflect.Descriptor instead.
+func (*ListArtifactsResponse_MonolithArtifact) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetWorkflowRunBackendId() string {
+	if x != nil {
+		return x.WorkflowRunBackendId
+	}
+	return ""
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetWorkflowJobRunBackendId() string {
+	if x != nil {
+		return x.WorkflowJobRunBackendId
+	}
+	return ""
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetDatabaseId() int64 {
+	if x != nil {
+		return x.DatabaseId
+	}
+	return 0
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetSize() int64 {
+	if x != nil {
+		return x.Size
+	}
+	return 0
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetCreatedAt() *timestamppb.Timestamp {
+	if x != nil {
+		return x.CreatedAt
+	}
+	return nil
+}
+
+type GetSignedArtifactURLRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WorkflowRunBackendId    string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+	WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+	Name                    string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+}
+
+func (x *GetSignedArtifactURLRequest) Reset() {
+	*x = GetSignedArtifactURLRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[7]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetSignedArtifactURLRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetSignedArtifactURLRequest) ProtoMessage() {}
+
+func (x *GetSignedArtifactURLRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[7]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetSignedArtifactURLRequest.ProtoReflect.Descriptor instead.
+func (*GetSignedArtifactURLRequest) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *GetSignedArtifactURLRequest) GetWorkflowRunBackendId() string {
+	if x != nil {
+		return x.WorkflowRunBackendId
+	}
+	return ""
+}
+
+func (x *GetSignedArtifactURLRequest) GetWorkflowJobRunBackendId() string {
+	if x != nil {
+		return x.WorkflowJobRunBackendId
+	}
+	return ""
+}
+
+func (x *GetSignedArtifactURLRequest) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+type GetSignedArtifactURLResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"`
+}
+
+func (x *GetSignedArtifactURLResponse) Reset() {
+	*x = GetSignedArtifactURLResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[8]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetSignedArtifactURLResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetSignedArtifactURLResponse) ProtoMessage() {}
+
+func (x *GetSignedArtifactURLResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[8]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetSignedArtifactURLResponse.ProtoReflect.Descriptor instead.
+func (*GetSignedArtifactURLResponse) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *GetSignedArtifactURLResponse) GetSignedUrl() string {
+	if x != nil {
+		return x.SignedUrl
+	}
+	return ""
+}
+
+type DeleteArtifactRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WorkflowRunBackendId    string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+	WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+	Name                    string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+}
+
+func (x *DeleteArtifactRequest) Reset() {
+	*x = DeleteArtifactRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[9]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *DeleteArtifactRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteArtifactRequest) ProtoMessage() {}
+
+func (x *DeleteArtifactRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[9]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteArtifactRequest.ProtoReflect.Descriptor instead.
+func (*DeleteArtifactRequest) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *DeleteArtifactRequest) GetWorkflowRunBackendId() string {
+	if x != nil {
+		return x.WorkflowRunBackendId
+	}
+	return ""
+}
+
+func (x *DeleteArtifactRequest) GetWorkflowJobRunBackendId() string {
+	if x != nil {
+		return x.WorkflowJobRunBackendId
+	}
+	return ""
+}
+
+func (x *DeleteArtifactRequest) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+type DeleteArtifactResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Ok         bool  `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
+	ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"`
+}
+
+func (x *DeleteArtifactResponse) Reset() {
+	*x = DeleteArtifactResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_artifact_proto_msgTypes[10]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *DeleteArtifactResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteArtifactResponse) ProtoMessage() {}
+
+func (x *DeleteArtifactResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_artifact_proto_msgTypes[10]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteArtifactResponse.ProtoReflect.Descriptor instead.
+func (*DeleteArtifactResponse) Descriptor() ([]byte, []int) {
+	return file_artifact_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *DeleteArtifactResponse) GetOk() bool {
+	if x != nil {
+		return x.Ok
+	}
+	return false
+}
+
+func (x *DeleteArtifactResponse) GetArtifactId() int64 {
+	if x != nil {
+		return x.ArtifactId
+	}
+	return 0
+}
+
+var File_artifact_proto protoreflect.FileDescriptor
+
+var file_artifact_proto_rawDesc = []byte{
+	0x0a, 0x0e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x12, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73,
+	0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a,
+	0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
+	0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
+	0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x22, 0xf5, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66,
+	0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f,
+	0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
+	0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72,
+	0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49,
+	0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f,
+	0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77,
+	0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12,
+	0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
+	0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61,
+	0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
+	0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18,
+	0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52,
+	0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61,
+	0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+	0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02,
+	0x6f, 0x6b, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x75, 0x70, 0x6c,
+	0x6f, 0x61, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73,
+	0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xe8,
+	0x01, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66,
+	0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f,
+	0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
+	0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72,
+	0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49,
+	0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f,
+	0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77,
+	0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12,
+	0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
+	0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
+	0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18,
+	0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61,
+	0x6c, 0x75, 0x65, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x4b, 0x0a, 0x18, 0x46, 0x69, 0x6e,
+	0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73,
+	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63,
+	0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69,
+	0x66, 0x61, 0x63, 0x74, 0x49, 0x64, 0x22, 0x84, 0x02, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41,
+	0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+	0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f,
+	0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
+	0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
+	0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
+	0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72,
+	0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65,
+	0x6e, 0x64, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x69, 0x6c,
+	0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+	0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69,
+	0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x69, 0x6c,
+	0x74, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72,
+	0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61,
+	0x6c, 0x75, 0x65, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x7c, 0x0a,
+	0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61,
+	0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x67, 0x69, 0x74, 0x68,
+	0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c,
+	0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72,
+	0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f,
+	0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74,
+	0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x26,
+	0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73,
+	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72,
+	0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
+	0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69,
+	0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f,
+	0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a,
+	0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75,
+	0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52,
+	0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x64,
+	0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
+	0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04,
+	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+	0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04,
+	0x73, 0x69, 0x7a, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f,
+	0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
+	0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
+	0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22,
+	0xa6, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74,
+	0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+	0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f,
+	0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
+	0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
+	0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
+	0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72,
+	0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65,
+	0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x53,
+	0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e,
+	0x65, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69,
+	0x67, 0x6e, 0x65, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xa0, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65,
+	0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+	0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75,
+	0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42,
+	0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b,
+	0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63,
+	0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77,
+	0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
+	0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x44, 0x65,
+	0x6c, 0x65, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
+	0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74,
+	0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66,
+	0x61, 0x63, 0x74, 0x49, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_artifact_proto_rawDescOnce sync.Once
+	file_artifact_proto_rawDescData = file_artifact_proto_rawDesc
+)
+
+func file_artifact_proto_rawDescGZIP() []byte {
+	file_artifact_proto_rawDescOnce.Do(func() {
+		file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(file_artifact_proto_rawDescData)
+	})
+	return file_artifact_proto_rawDescData
+}
+
+var (
+	file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
+	file_artifact_proto_goTypes  = []interface{}{
+		(*CreateArtifactRequest)(nil),                  // 0: github.actions.results.api.v1.CreateArtifactRequest
+		(*CreateArtifactResponse)(nil),                 // 1: github.actions.results.api.v1.CreateArtifactResponse
+		(*FinalizeArtifactRequest)(nil),                // 2: github.actions.results.api.v1.FinalizeArtifactRequest
+		(*FinalizeArtifactResponse)(nil),               // 3: github.actions.results.api.v1.FinalizeArtifactResponse
+		(*ListArtifactsRequest)(nil),                   // 4: github.actions.results.api.v1.ListArtifactsRequest
+		(*ListArtifactsResponse)(nil),                  // 5: github.actions.results.api.v1.ListArtifactsResponse
+		(*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact
+		(*GetSignedArtifactURLRequest)(nil),            // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest
+		(*GetSignedArtifactURLResponse)(nil),           // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse
+		(*DeleteArtifactRequest)(nil),                  // 9: github.actions.results.api.v1.DeleteArtifactRequest
+		(*DeleteArtifactResponse)(nil),                 // 10: github.actions.results.api.v1.DeleteArtifactResponse
+		(*timestamppb.Timestamp)(nil),                  // 11: google.protobuf.Timestamp
+		(*wrapperspb.StringValue)(nil),                 // 12: google.protobuf.StringValue
+		(*wrapperspb.Int64Value)(nil),                  // 13: google.protobuf.Int64Value
+	}
+)
+
+var file_artifact_proto_depIdxs = []int32{
+	11, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp
+	12, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue
+	12, // 2: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue
+	13, // 3: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value
+	6,  // 4: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact
+	11, // 5: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp
+	6,  // [6:6] is the sub-list for method output_type
+	6,  // [6:6] is the sub-list for method input_type
+	6,  // [6:6] is the sub-list for extension type_name
+	6,  // [6:6] is the sub-list for extension extendee
+	0,  // [0:6] is the sub-list for field type_name
+}
+
+func init() { file_artifact_proto_init() }
+func file_artifact_proto_init() {
+	if File_artifact_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_artifact_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CreateArtifactRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CreateArtifactResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*FinalizeArtifactRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*FinalizeArtifactResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListArtifactsRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListArtifactsResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListArtifactsResponse_MonolithArtifact); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetSignedArtifactURLRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetSignedArtifactURLResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*DeleteArtifactRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_artifact_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*DeleteArtifactResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_artifact_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   11,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_artifact_proto_goTypes,
+		DependencyIndexes: file_artifact_proto_depIdxs,
+		MessageInfos:      file_artifact_proto_msgTypes,
+	}.Build()
+	File_artifact_proto = out.File
+	file_artifact_proto_rawDesc = nil
+	file_artifact_proto_goTypes = nil
+	file_artifact_proto_depIdxs = nil
+}
diff --git a/routers/api/actions/artifact.proto b/routers/api/actions/artifact.proto
new file mode 100644
index 0000000000..c68e5d030d
--- /dev/null
+++ b/routers/api/actions/artifact.proto
@@ -0,0 +1,73 @@
+syntax = "proto3";
+
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/wrappers.proto";
+
+package github.actions.results.api.v1;
+
+message CreateArtifactRequest {
+    string workflow_run_backend_id = 1;
+    string workflow_job_run_backend_id = 2;
+    string name = 3;
+    google.protobuf.Timestamp expires_at = 4;
+    int32 version = 5;
+}
+
+message CreateArtifactResponse {
+    bool ok = 1;
+    string signed_upload_url = 2;
+}
+
+message FinalizeArtifactRequest {
+    string workflow_run_backend_id = 1;
+    string workflow_job_run_backend_id = 2;
+    string name = 3;
+    int64 size = 4;
+    google.protobuf.StringValue hash = 5;
+}
+
+message FinalizeArtifactResponse {
+  bool ok = 1;
+  int64 artifact_id = 2;
+}
+
+message ListArtifactsRequest {
+    string workflow_run_backend_id = 1;
+    string workflow_job_run_backend_id = 2;
+    google.protobuf.StringValue name_filter = 3;
+    google.protobuf.Int64Value id_filter = 4;
+}
+
+message ListArtifactsResponse {
+    repeated ListArtifactsResponse_MonolithArtifact artifacts = 1;
+}
+
+message ListArtifactsResponse_MonolithArtifact {
+    string workflow_run_backend_id = 1;
+    string workflow_job_run_backend_id = 2;
+    int64 database_id = 3;
+    string name = 4;
+    int64 size = 5;
+    google.protobuf.Timestamp created_at = 6;
+}
+
+message GetSignedArtifactURLRequest {
+    string workflow_run_backend_id = 1;
+    string workflow_job_run_backend_id = 2;
+    string name = 3;
+}
+
+message GetSignedArtifactURLResponse {
+    string signed_url = 1;
+}
+
+message DeleteArtifactRequest {
+    string workflow_run_backend_id = 1;
+    string workflow_job_run_backend_id = 2;
+    string name = 3;
+}
+
+message DeleteArtifactResponse {
+    bool ok = 1;
+    int64 artifact_id = 2;
+}
diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go
index 3363c4c0e8..d530e9cee5 100644
--- a/routers/api/actions/artifacts.go
+++ b/routers/api/actions/artifacts.go
@@ -63,6 +63,7 @@ package actions
 
 import (
 	"crypto/md5"
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -70,7 +71,6 @@ import (
 
 	"code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
@@ -79,6 +79,7 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	web_types "code.gitea.io/gitea/modules/web/types"
 	actions_service "code.gitea.io/gitea/services/actions"
+	"code.gitea.io/gitea/services/context"
 )
 
 const artifactRouteBase = "/_apis/pipelines/workflows/{run_id}/artifacts"
@@ -426,7 +427,19 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
 
 	var items []downloadArtifactResponseItem
 	for _, artifact := range artifacts {
-		downloadURL := ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
+		var downloadURL string
+		if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
+			u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName)
+			if err != nil && !errors.Is(err, storage.ErrURLNotSupported) {
+				log.Error("Error getting serve direct url: %v", err)
+			}
+			if u != nil {
+				downloadURL = u.String()
+			}
+		}
+		if downloadURL == "" {
+			downloadURL = ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
+		}
 		item := downloadArtifactResponseItem{
 			Path:            util.PathJoinRel(itemPath, artifact.ArtifactPath),
 			ItemType:        "file",
diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go
index 0713c8bba8..3a81724b3a 100644
--- a/routers/api/actions/artifacts_chunks.go
+++ b/routers/api/actions/artifacts_chunks.go
@@ -5,11 +5,16 @@ package actions
 
 import (
 	"crypto/md5"
+	"crypto/sha256"
 	"encoding/base64"
+	"encoding/hex"
+	"errors"
 	"fmt"
+	"hash"
 	"io"
 	"path/filepath"
 	"sort"
+	"strings"
 	"time"
 
 	"code.gitea.io/gitea/models/actions"
@@ -18,6 +23,52 @@ import (
 	"code.gitea.io/gitea/modules/storage"
 )
 
+func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
+	artifact *actions.ActionArtifact,
+	contentSize, runID, start, end, length int64, checkMd5 bool,
+) (int64, error) {
+	// build chunk store path
+	storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end)
+	var r io.Reader = ctx.Req.Body
+	var hasher hash.Hash
+	if checkMd5 {
+		// use io.TeeReader to avoid reading all body to md5 sum.
+		// it writes data to hasher after reading end
+		// if hash is not matched, delete the read-end result
+		hasher = md5.New()
+		r = io.TeeReader(r, hasher)
+	}
+	// save chunk to storage
+	writtenSize, err := st.Save(storagePath, r, -1)
+	if err != nil {
+		return -1, fmt.Errorf("save chunk to storage error: %v", err)
+	}
+	var checkErr error
+	if checkMd5 {
+		// check md5
+		reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
+		chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
+		log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
+		// if md5 not match, delete the chunk
+		if reqMd5String != chunkMd5String {
+			checkErr = fmt.Errorf("md5 not match")
+		}
+	}
+	if writtenSize != contentSize {
+		checkErr = errors.Join(checkErr, fmt.Errorf("contentSize not match body size"))
+	}
+	if checkErr != nil {
+		if err := st.Delete(storagePath); err != nil {
+			log.Error("Error deleting chunk: %s, %v", storagePath, err)
+		}
+		return -1, checkErr
+	}
+	log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
+		storagePath, contentSize, artifact.ID, start, end)
+	// return chunk total size
+	return length, nil
+}
+
 func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
 	artifact *actions.ActionArtifact,
 	contentSize, runID int64,
@@ -29,33 +80,15 @@ func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
 		log.Warn("parse content range error: %v, content-range: %s", err, contentRange)
 		return -1, fmt.Errorf("parse content range error: %v", err)
 	}
-	// build chunk store path
-	storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end)
-	// use io.TeeReader to avoid reading all body to md5 sum.
-	// it writes data to hasher after reading end
-	// if hash is not matched, delete the read-end result
-	hasher := md5.New()
-	r := io.TeeReader(ctx.Req.Body, hasher)
-	// save chunk to storage
-	writtenSize, err := st.Save(storagePath, r, -1)
-	if err != nil {
-		return -1, fmt.Errorf("save chunk to storage error: %v", err)
-	}
-	// check md5
-	reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
-	chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
-	log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
-	// if md5 not match, delete the chunk
-	if reqMd5String != chunkMd5String || writtenSize != contentSize {
-		if err := st.Delete(storagePath); err != nil {
-			log.Error("Error deleting chunk: %s, %v", storagePath, err)
-		}
-		return -1, fmt.Errorf("md5 not match")
-	}
-	log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
-		storagePath, contentSize, artifact.ID, start, end)
-	// return chunk total size
-	return length, nil
+	return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, length, true)
+}
+
+func appendUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
+	artifact *actions.ActionArtifact,
+	start, contentSize, runID int64,
+) (int64, error) {
+	end := start + contentSize - 1
+	return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, contentSize, false)
 }
 
 type chunkFileItem struct {
@@ -111,14 +144,14 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int
 			log.Debug("artifact %d chunks not found", art.ID)
 			continue
 		}
-		if err := mergeChunksForArtifact(ctx, chunks, st, art); err != nil {
+		if err := mergeChunksForArtifact(ctx, chunks, st, art, ""); err != nil {
 			return err
 		}
 	}
 	return nil
 }
 
-func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact) error {
+func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact, checksum string) error {
 	sort.Slice(chunks, func(i, j int) bool {
 		return chunks[i].Start < chunks[j].Start
 	})
@@ -157,6 +190,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
 		readers = append(readers, readCloser)
 	}
 	mergedReader := io.MultiReader(readers...)
+	shaPrefix := "sha256:"
+	var hash hash.Hash
+	if strings.HasPrefix(checksum, shaPrefix) {
+		hash = sha256.New()
+	}
+	if hash != nil {
+		mergedReader = io.TeeReader(mergedReader, hash)
+	}
 
 	// if chunk is gzip, use gz as extension
 	// download-artifact action will use content-encoding header to decide if it should decompress the file
@@ -185,6 +226,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
 		}
 	}()
 
+	if hash != nil {
+		rawChecksum := hash.Sum(nil)
+		actualChecksum := hex.EncodeToString(rawChecksum)
+		if !strings.HasSuffix(checksum, actualChecksum) {
+			return fmt.Errorf("update artifact error checksum is invalid")
+		}
+	}
+
 	// save storage path to artifact
 	log.Debug("[artifact] merge chunks to artifact: %d, %s, old:%s", artifact.ID, storagePath, artifact.StoragePath)
 	// if artifact is already uploaded, delete the old file
diff --git a/routers/api/actions/artifacts_utils.go b/routers/api/actions/artifacts_utils.go
index 381e7eb16e..aaf89ef40e 100644
--- a/routers/api/actions/artifacts_utils.go
+++ b/routers/api/actions/artifacts_utils.go
@@ -43,6 +43,17 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
 	return task, runID, true
 }
 
+func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) {
+	task := ctx.ActionTask
+	runID, err := strconv.ParseInt(rawRunID, 10, 64)
+	if err != nil || task.Job.RunID != runID {
+		log.Error("Error runID not match")
+		ctx.Error(http.StatusBadRequest, "run-id does not match")
+		return nil, 0, false
+	}
+	return task, runID, true
+}
+
 func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool {
 	paramHash := ctx.Params("artifact_hash")
 	// use artifact name to create upload url
diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go
new file mode 100644
index 0000000000..8300989c75
--- /dev/null
+++ b/routers/api/actions/artifactsv4.go
@@ -0,0 +1,512 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+// GitHub Actions Artifacts V4 API Simple Description
+//
+// 1. Upload artifact
+// 1.1. CreateArtifact
+// Post: /twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact
+// Request:
+// {
+//     "workflow_run_backend_id": "21",
+//     "workflow_job_run_backend_id": "49",
+//     "name": "test",
+//     "version": 4
+// }
+// Response:
+// {
+//     "ok": true,
+//     "signedUploadUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75"
+// }
+// 1.2. Upload Zip Content to Blobstorage (unauthenticated request)
+// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=block
+// 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded
+// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock
+// 1.4. Unknown xml payload to Blobstorage (unauthenticated request), ignored for now
+// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList
+// 1.5. FinalizeArtifact
+// Post: /twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact
+// Request
+// {
+//     "workflow_run_backend_id": "21",
+//     "workflow_job_run_backend_id": "49",
+//     "name": "test",
+//     "size": "2097",
+//     "hash": "sha256:b6325614d5649338b87215d9536b3c0477729b8638994c74cdefacb020a2cad4"
+// }
+// Response
+// {
+//     "ok": true,
+//     "artifactId": "4"
+// }
+// 2. Download artifact
+// 2.1. ListArtifacts and optionally filter by artifact exact name or id
+// Post: /twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts
+// Request
+// {
+//     "workflow_run_backend_id": "21",
+//     "workflow_job_run_backend_id": "49",
+//     "name_filter": "test"
+// }
+// Response
+// {
+//     "artifacts": [
+//         {
+//             "workflowRunBackendId": "21",
+//             "workflowJobRunBackendId": "49",
+//             "databaseId": "4",
+//             "name": "test",
+//             "size": "2093",
+//             "createdAt": "2024-01-23T00:13:28Z"
+//         }
+//     ]
+// }
+// 2.2. GetSignedArtifactURL get the URL to download the artifact zip file of a specific artifact
+// Post: /twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL
+// Request
+// {
+//     "workflow_run_backend_id": "21",
+//     "workflow_job_run_backend_id": "49",
+//     "name": "test"
+// }
+// Response
+// {
+//     "signedUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76"
+// }
+// 2.3. Download Zip from Blobstorage (unauthenticated request)
+// GET: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/base64"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+
+	"code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/storage"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
+
+	"google.golang.org/protobuf/encoding/protojson"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	"google.golang.org/protobuf/types/known/timestamppb"
+)
+
+const (
+	ArtifactV4RouteBase       = "/twirp/github.actions.results.api.v1.ArtifactService"
+	ArtifactV4ContentEncoding = "application/zip"
+)
+
+type artifactV4Routes struct {
+	prefix string
+	fs     storage.ObjectStorage
+}
+
+func ArtifactV4Contexter() func(next http.Handler) http.Handler {
+	return func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+			base, baseCleanUp := context.NewBaseContext(resp, req)
+			defer baseCleanUp()
+
+			ctx := &ArtifactContext{Base: base}
+			ctx.AppendContextValue(artifactContextKey, ctx)
+
+			next.ServeHTTP(ctx.Resp, ctx.Req)
+		})
+	}
+}
+
+func ArtifactsV4Routes(prefix string) *web.Route {
+	m := web.NewRoute()
+
+	r := artifactV4Routes{
+		prefix: prefix,
+		fs:     storage.ActionsArtifacts,
+	}
+
+	m.Group("", func() {
+		m.Post("CreateArtifact", r.createArtifact)
+		m.Post("FinalizeArtifact", r.finalizeArtifact)
+		m.Post("ListArtifacts", r.listArtifacts)
+		m.Post("GetSignedArtifactURL", r.getSignedArtifactURL)
+		m.Post("DeleteArtifact", r.deleteArtifact)
+	}, ArtifactContexter())
+	m.Group("", func() {
+		m.Put("UploadArtifact", r.uploadArtifact)
+		m.Get("DownloadArtifact", r.downloadArtifact)
+	}, ArtifactV4Contexter())
+
+	return m
+}
+
+func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID int64) []byte {
+	mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
+	mac.Write([]byte(endp))
+	mac.Write([]byte(expires))
+	mac.Write([]byte(artifactName))
+	mac.Write([]byte(fmt.Sprint(taskID)))
+	return mac.Sum(nil)
+}
+
+func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID int64) string {
+	expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST")
+	uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") +
+		"/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID)
+	return uploadURL
+}
+
+func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) {
+	rawTaskID := ctx.Req.URL.Query().Get("taskID")
+	sig := ctx.Req.URL.Query().Get("sig")
+	expires := ctx.Req.URL.Query().Get("expires")
+	artifactName := ctx.Req.URL.Query().Get("artifactName")
+	dsig, _ := base64.URLEncoding.DecodeString(sig)
+	taskID, _ := strconv.ParseInt(rawTaskID, 10, 64)
+
+	expecedsig := r.buildSignature(endp, expires, artifactName, taskID)
+	if !hmac.Equal(dsig, expecedsig) {
+		log.Error("Error unauthorized")
+		ctx.Error(http.StatusUnauthorized, "Error unauthorized")
+		return nil, "", false
+	}
+	t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires)
+	if err != nil || t.Before(time.Now()) {
+		log.Error("Error link expired")
+		ctx.Error(http.StatusUnauthorized, "Error link expired")
+		return nil, "", false
+	}
+	task, err := actions.GetTaskByID(ctx, taskID)
+	if err != nil {
+		log.Error("Error runner api getting task by ID: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID")
+		return nil, "", false
+	}
+	if task.Status != actions.StatusRunning {
+		log.Error("Error runner api getting task: task is not running")
+		ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
+		return nil, "", false
+	}
+	if err := task.LoadJob(ctx); err != nil {
+		log.Error("Error runner api getting job: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error runner api getting job")
+		return nil, "", false
+	}
+	return task, artifactName, true
+}
+
+func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) {
+	var art actions.ActionArtifact
+	has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ? AND content_encoding = ?", runID, name, name+".zip", ArtifactV4ContentEncoding).Get(&art)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, util.ErrNotExist
+	}
+	return &art, nil
+}
+
+func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool {
+	body, err := io.ReadAll(ctx.Req.Body)
+	if err != nil {
+		log.Error("Error decode request body: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error decode request body")
+		return false
+	}
+	err = protojson.Unmarshal(body, req)
+	if err != nil {
+		log.Error("Error decode request body: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error decode request body")
+		return false
+	}
+	return true
+}
+
+func (r *artifactV4Routes) sendProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) {
+	resp, err := protojson.Marshal(req)
+	if err != nil {
+		log.Error("Error encode response body: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error encode response body")
+		return
+	}
+	ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
+	ctx.Resp.WriteHeader(http.StatusOK)
+	_, _ = ctx.Resp.Write(resp)
+}
+
+func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) {
+	var req CreateArtifactRequest
+
+	if ok := r.parseProtbufBody(ctx, &req); !ok {
+		return
+	}
+	_, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+	if !ok {
+		return
+	}
+
+	artifactName := req.Name
+
+	rententionDays := setting.Actions.ArtifactRetentionDays
+	if req.ExpiresAt != nil {
+		rententionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24)
+	}
+	// create or get artifact with name and path
+	artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", rententionDays)
+	if err != nil {
+		log.Error("Error create or get artifact: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error create or get artifact")
+		return
+	}
+	artifact.ContentEncoding = ArtifactV4ContentEncoding
+	if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
+		log.Error("Error UpdateArtifactByID: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID")
+		return
+	}
+
+	respData := CreateArtifactResponse{
+		Ok:              true,
+		SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, ctx.ActionTask.ID),
+	}
+	r.sendProtbufBody(ctx, &respData)
+}
+
+func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
+	task, artifactName, ok := r.verifySignature(ctx, "UploadArtifact")
+	if !ok {
+		return
+	}
+
+	comp := ctx.Req.URL.Query().Get("comp")
+	switch comp {
+	case "block", "appendBlock":
+		// get artifact by name
+		artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
+		if err != nil {
+			log.Error("Error artifact not found: %v", err)
+			ctx.Error(http.StatusNotFound, "Error artifact not found")
+			return
+		}
+
+		if comp == "block" {
+			artifact.FileSize = 0
+			artifact.FileCompressedSize = 0
+		}
+
+		_, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID)
+		if err != nil {
+			log.Error("Error runner api getting task: task is not running")
+			ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
+			return
+		}
+		artifact.FileCompressedSize += ctx.Req.ContentLength
+		artifact.FileSize += ctx.Req.ContentLength
+		if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
+			log.Error("Error UpdateArtifactByID: %v", err)
+			ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID")
+			return
+		}
+		ctx.JSON(http.StatusCreated, "appended")
+	case "blocklist":
+		ctx.JSON(http.StatusCreated, "created")
+	}
+}
+
+func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) {
+	var req FinalizeArtifactRequest
+
+	if ok := r.parseProtbufBody(ctx, &req); !ok {
+		return
+	}
+	_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+	if !ok {
+		return
+	}
+
+	// get artifact by name
+	artifact, err := r.getArtifactByName(ctx, runID, req.Name)
+	if err != nil {
+		log.Error("Error artifact not found: %v", err)
+		ctx.Error(http.StatusNotFound, "Error artifact not found")
+		return
+	}
+	chunkMap, err := listChunksByRunID(r.fs, runID)
+	if err != nil {
+		log.Error("Error merge chunks: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error merge chunks")
+		return
+	}
+	chunks, ok := chunkMap[artifact.ID]
+	if !ok {
+		log.Error("Error merge chunks")
+		ctx.Error(http.StatusInternalServerError, "Error merge chunks")
+		return
+	}
+	checksum := ""
+	if req.Hash != nil {
+		checksum = req.Hash.Value
+	}
+	if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil {
+		log.Error("Error merge chunks: %v", err)
+		ctx.Error(http.StatusInternalServerError, "Error merge chunks")
+		return
+	}
+
+	respData := FinalizeArtifactResponse{
+		Ok:         true,
+		ArtifactId: artifact.ID,
+	}
+	r.sendProtbufBody(ctx, &respData)
+}
+
+func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
+	var req ListArtifactsRequest
+
+	if ok := r.parseProtbufBody(ctx, &req); !ok {
+		return
+	}
+	_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+	if !ok {
+		return
+	}
+
+	artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID})
+	if err != nil {
+		log.Error("Error getting artifacts: %v", err)
+		ctx.Error(http.StatusInternalServerError, err.Error())
+		return
+	}
+	if len(artifacts) == 0 {
+		log.Debug("[artifact] handleListArtifacts, no artifacts")
+		ctx.Error(http.StatusNotFound)
+		return
+	}
+
+	list := []*ListArtifactsResponse_MonolithArtifact{}
+
+	table := map[string]*ListArtifactsResponse_MonolithArtifact{}
+	for _, artifact := range artifacts {
+		if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != ArtifactV4ContentEncoding {
+			table[artifact.ArtifactName] = nil
+			continue
+		}
+
+		table[artifact.ArtifactName] = &ListArtifactsResponse_MonolithArtifact{
+			Name:                    artifact.ArtifactName,
+			CreatedAt:               timestamppb.New(artifact.CreatedUnix.AsTime()),
+			DatabaseId:              artifact.ID,
+			WorkflowRunBackendId:    req.WorkflowRunBackendId,
+			WorkflowJobRunBackendId: req.WorkflowJobRunBackendId,
+			Size:                    artifact.FileSize,
+		}
+	}
+	for _, artifact := range table {
+		if artifact != nil {
+			list = append(list, artifact)
+		}
+	}
+
+	respData := ListArtifactsResponse{
+		Artifacts: list,
+	}
+	r.sendProtbufBody(ctx, &respData)
+}
+
+func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
+	var req GetSignedArtifactURLRequest
+
+	if ok := r.parseProtbufBody(ctx, &req); !ok {
+		return
+	}
+	_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+	if !ok {
+		return
+	}
+
+	artifactName := req.Name
+
+	// get artifact by name
+	artifact, err := r.getArtifactByName(ctx, runID, artifactName)
+	if err != nil {
+		log.Error("Error artifact not found: %v", err)
+		ctx.Error(http.StatusNotFound, "Error artifact not found")
+		return
+	}
+
+	respData := GetSignedArtifactURLResponse{}
+
+	if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
+		u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath)
+		if u != nil && err == nil {
+			respData.SignedUrl = u.String()
+		}
+	}
+	if respData.SignedUrl == "" {
+		respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, ctx.ActionTask.ID)
+	}
+	r.sendProtbufBody(ctx, &respData)
+}
+
+func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) {
+	task, artifactName, ok := r.verifySignature(ctx, "DownloadArtifact")
+	if !ok {
+		return
+	}
+
+	// get artifact by name
+	artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
+	if err != nil {
+		log.Error("Error artifact not found: %v", err)
+		ctx.Error(http.StatusNotFound, "Error artifact not found")
+		return
+	}
+
+	file, _ := r.fs.Open(artifact.StoragePath)
+
+	_, _ = io.Copy(ctx.Resp, file)
+}
+
+func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) {
+	var req DeleteArtifactRequest
+
+	if ok := r.parseProtbufBody(ctx, &req); !ok {
+		return
+	}
+	_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+	if !ok {
+		return
+	}
+
+	// get artifact by name
+	artifact, err := r.getArtifactByName(ctx, runID, req.Name)
+	if err != nil {
+		log.Error("Error artifact not found: %v", err)
+		ctx.Error(http.StatusNotFound, "Error artifact not found")
+		return
+	}
+
+	err = actions.SetArtifactNeedDelete(ctx, runID, req.Name)
+	if err != nil {
+		log.Error("Error deleting artifacts: %v", err)
+		ctx.Error(http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	respData := DeleteArtifactResponse{
+		Ok:         true,
+		ArtifactId: artifact.ID,
+	}
+	r.sendProtbufBody(ctx, &respData)
+}
diff --git a/routers/api/actions/ping/ping.go b/routers/api/actions/ping/ping.go
index 55219fe12b..828350407a 100644
--- a/routers/api/actions/ping/ping.go
+++ b/routers/api/actions/ping/ping.go
@@ -12,7 +12,7 @@ import (
 
 	pingv1 "code.gitea.io/actions-proto-go/ping/v1"
 	"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
-	"github.com/bufbuild/connect-go"
+	"connectrpc.com/connect"
 )
 
 func NewPingServiceHandler() (string, http.Handler) {
@@ -21,9 +21,7 @@ func NewPingServiceHandler() (string, http.Handler) {
 
 var _ pingv1connect.PingServiceHandler = (*Service)(nil)
 
-type Service struct {
-	pingv1connect.UnimplementedPingServiceHandler
-}
+type Service struct{}
 
 func (s *Service) Ping(
 	ctx context.Context,
diff --git a/routers/api/actions/ping/ping_test.go b/routers/api/actions/ping/ping_test.go
index f39e94a1f3..098b003ea2 100644
--- a/routers/api/actions/ping/ping_test.go
+++ b/routers/api/actions/ping/ping_test.go
@@ -11,7 +11,7 @@ import (
 
 	pingv1 "code.gitea.io/actions-proto-go/ping/v1"
 	"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
-	"github.com/bufbuild/connect-go"
+	"connectrpc.com/connect"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
diff --git a/routers/api/actions/runner/interceptor.go b/routers/api/actions/runner/interceptor.go
index ddc754dbc7..c2f4ade174 100644
--- a/routers/api/actions/runner/interceptor.go
+++ b/routers/api/actions/runner/interceptor.go
@@ -15,7 +15,7 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
-	"github.com/bufbuild/connect-go"
+	"connectrpc.com/connect"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 )
diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go
index 8df6f297ce..b2f3e7af78 100644
--- a/routers/api/actions/runner/runner.go
+++ b/routers/api/actions/runner/runner.go
@@ -9,6 +9,8 @@ import (
 	"net/http"
 
 	actions_model "code.gitea.io/gitea/models/actions"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/util"
@@ -16,7 +18,7 @@ import (
 
 	runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
 	"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
-	"github.com/bufbuild/connect-go"
+	"connectrpc.com/connect"
 	gouuid "github.com/google/uuid"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
@@ -32,9 +34,7 @@ func NewRunnerServiceHandler() (string, http.Handler) {
 
 var _ runnerv1connect.RunnerServiceClient = (*Service)(nil)
 
-type Service struct {
-	runnerv1connect.UnimplementedRunnerServiceHandler
-}
+type Service struct{}
 
 // Register for new runner.
 func (s *Service) Register(
@@ -54,6 +54,18 @@ func (s *Service) Register(
 		return nil, errors.New("runner registration token has been invalidated, please use the latest one")
 	}
 
+	if runnerToken.OwnerID > 0 {
+		if _, err := user_model.GetUserByID(ctx, runnerToken.OwnerID); err != nil {
+			return nil, errors.New("owner of the token not found")
+		}
+	}
+
+	if runnerToken.RepoID > 0 {
+		if _, err := repo_model.GetRepositoryByID(ctx, runnerToken.RepoID); err != nil {
+			return nil, errors.New("repository of the token not found")
+		}
+	}
+
 	labels := req.Msg.Labels
 	// TODO: agent_labels should be removed from pb after Gitea 1.20 released.
 	// Old version runner's agent_labels slice is not empty and labels slice is empty.
diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go
index a7cb31288c..ff6ec5bd54 100644
--- a/routers/api/actions/runner/utils.go
+++ b/routers/api/actions/runner/utils.go
@@ -15,7 +15,6 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
-	secret_module "code.gitea.io/gitea/modules/secret"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/services/actions"
 
@@ -32,14 +31,24 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
 		return nil, false, nil
 	}
 
+	secrets, err := secret_model.GetSecretsOfTask(ctx, t)
+	if err != nil {
+		return nil, false, fmt.Errorf("GetSecretsOfTask: %w", err)
+	}
+
+	vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run)
+	if err != nil {
+		return nil, false, fmt.Errorf("GetVariablesOfRun: %w", err)
+	}
+
 	actions.CreateCommitStatus(ctx, t.Job)
 
 	task := &runnerv1.Task{
 		Id:              t.ID,
 		WorkflowPayload: t.Job.WorkflowPayload,
 		Context:         generateTaskContext(t),
-		Secrets:         getSecretsOfTask(ctx, t),
-		Vars:            getVariablesOfTask(ctx, t),
+		Secrets:         secrets,
+		Vars:            vars,
 	}
 
 	if needs, err := findTaskNeeds(ctx, t); err != nil {
@@ -55,71 +64,6 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
 	return task, true, nil
 }
 
-func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
-	secrets := map[string]string{}
-
-	secrets["GITHUB_TOKEN"] = task.Token
-	secrets["GITEA_TOKEN"] = task.Token
-
-	if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget {
-		// ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated.
-		// for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch
-		// see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
-		return secrets
-	}
-
-	ownerSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID})
-	if err != nil {
-		log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err)
-		// go on
-	}
-	repoSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{RepoID: task.Job.Run.RepoID})
-	if err != nil {
-		log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err)
-		// go on
-	}
-
-	for _, secret := range append(ownerSecrets, repoSecrets...) {
-		if v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data); err != nil {
-			log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err)
-			// go on
-		} else {
-			secrets[secret.Name] = v
-		}
-	}
-
-	return secrets
-}
-
-func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
-	variables := map[string]string{}
-
-	// Global
-	globalVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{})
-	if err != nil {
-		log.Error("find global variables: %v", err)
-	}
-
-	// Org / User level
-	ownerVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{OwnerID: task.Job.Run.Repo.OwnerID})
-	if err != nil {
-		log.Error("find variables of org: %d, error: %v", task.Job.Run.Repo.OwnerID, err)
-	}
-
-	// Repo level
-	repoVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{RepoID: task.Job.Run.RepoID})
-	if err != nil {
-		log.Error("find variables of repo: %d, error: %v", task.Job.Run.RepoID, err)
-	}
-
-	// Level precedence: Repo > Org / User > Global
-	for _, v := range append(globalVariables, append(ownerVariables, repoVariables...)...) {
-		variables[v.Name] = v.Data
-	}
-
-	return variables
-}
-
 func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
 	event := map[string]any{}
 	_ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event)
diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go
index bb14c5163a..dae9c3dfcb 100644
--- a/routers/api/packages/alpine/alpine.go
+++ b/routers/api/packages/alpine/alpine.go
@@ -14,12 +14,12 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 	alpine_service "code.gitea.io/gitea/services/packages/alpine"
 )
@@ -72,7 +72,7 @@ func GetRepositoryFile(ctx *context.Context) {
 		ctx,
 		pv,
 		&packages_service.PackageFileInfo{
-			Filename:     alpine_service.IndexFilename,
+			Filename:     alpine_service.IndexArchiveFilename,
 			CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")),
 		},
 	)
@@ -182,19 +182,38 @@ func UploadPackageFile(ctx *context.Context) {
 }
 
 func DownloadPackageFile(ctx *context.Context) {
-	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
+	branch := ctx.Params("branch")
+	repository := ctx.Params("repository")
+	architecture := ctx.Params("architecture")
+
+	opts := &packages_model.PackageFileSearchOptions{
 		OwnerID:      ctx.Package.Owner.ID,
 		PackageType:  packages_model.TypeAlpine,
 		Query:        ctx.Params("filename"),
-		CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")),
-	})
+		CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
+	}
+	pfs, _, err := packages_model.SearchFiles(ctx, opts)
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
 		return
 	}
-	if len(pfs) != 1 {
-		apiError(ctx, http.StatusNotFound, nil)
-		return
+	if len(pfs) == 0 {
+		// Try again with architecture 'noarch'
+		if architecture == alpine_module.NoArch {
+			apiError(ctx, http.StatusNotFound, nil)
+			return
+		}
+
+		opts.CompositeKey = fmt.Sprintf("%s|%s|%s", branch, repository, alpine_module.NoArch)
+		if pfs, _, err = packages_model.SearchFiles(ctx, opts); err != nil {
+			apiError(ctx, http.StatusInternalServerError, err)
+			return
+		}
+
+		if len(pfs) == 0 {
+			apiError(ctx, http.StatusNotFound, nil)
+			return
+		}
 	}
 
 	s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0])
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index d990ebb56a..5e3cbac8f9 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -10,7 +10,6 @@ import (
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/perm"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
@@ -36,7 +35,7 @@ import (
 	"code.gitea.io/gitea/routers/api/packages/swift"
 	"code.gitea.io/gitea/routers/api/packages/vagrant"
 	"code.gitea.io/gitea/services/auth"
-	context_service "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
@@ -642,7 +641,7 @@ func CommonRoutes() *web.Route {
 				})
 			})
 		}, reqPackageAccess(perm.AccessModeRead))
-	}, context_service.UserAssignmentWeb(), context.PackageAssignment())
+	}, context.UserAssignmentWeb(), context.PackageAssignment())
 
 	return r
 }
@@ -812,7 +811,7 @@ func ContainerRoutes() *web.Route {
 
 			ctx.Status(http.StatusNotFound)
 		})
-	}, container.ReqContainerAccess, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
+	}, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
 
 	return r
 }
diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go
index 8f1e965c9a..140e532efd 100644
--- a/routers/api/packages/cargo/cargo.go
+++ b/routers/api/packages/cargo/cargo.go
@@ -12,14 +12,15 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	cargo_module "code.gitea.io/gitea/modules/packages/cargo"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	packages_service "code.gitea.io/gitea/services/packages"
 	cargo_service "code.gitea.io/gitea/services/packages/cargo"
@@ -110,7 +111,7 @@ func SearchPackages(ctx *context.Context) {
 			OwnerID:    ctx.Package.Owner.ID,
 			Type:       packages_model.TypeCargo,
 			Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 			Paginator:  &paginator,
 		},
 	)
diff --git a/routers/api/packages/chef/auth.go b/routers/api/packages/chef/auth.go
index 3aef8281a4..a790e9a363 100644
--- a/routers/api/packages/chef/auth.go
+++ b/routers/api/packages/chef/auth.go
@@ -8,6 +8,7 @@ import (
 	"crypto"
 	"crypto/rsa"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/pem"
@@ -26,8 +27,6 @@ import (
 	chef_module "code.gitea.io/gitea/modules/packages/chef"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/auth"
-
-	"github.com/minio/sha256-simd"
 )
 
 const (
diff --git a/routers/api/packages/chef/chef.go b/routers/api/packages/chef/chef.go
index f1e9ae12d8..b49f4e9d0a 100644
--- a/routers/api/packages/chef/chef.go
+++ b/routers/api/packages/chef/chef.go
@@ -15,12 +15,13 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	chef_module "code.gitea.io/gitea/modules/packages/chef"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
@@ -40,7 +41,7 @@ func PackagesUniverse(ctx *context.Context) {
 	pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages_model.TypeChef,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
@@ -85,7 +86,7 @@ func EnumeratePackages(ctx *context.Context) {
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages_model.TypeChef,
 		Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator: db.NewAbsoluteListOptions(
 			ctx.FormInt("start"),
 			ctx.FormInt("items"),
diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go
index 0551093cd1..a045da40de 100644
--- a/routers/api/packages/composer/composer.go
+++ b/routers/api/packages/composer/composer.go
@@ -14,12 +14,13 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	composer_module "code.gitea.io/gitea/modules/packages/composer"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	packages_service "code.gitea.io/gitea/services/packages"
 
@@ -66,7 +67,7 @@ func SearchPackages(ctx *context.Context) {
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages_model.TypeComposer,
 		Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator:  &paginator,
 	}
 	if ctx.FormTrim("type") != "" {
diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go
index 4bf13222dc..c45e085a4d 100644
--- a/routers/api/packages/conan/conan.go
+++ b/routers/api/packages/conan/conan.go
@@ -15,13 +15,13 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	conan_model "code.gitea.io/gitea/models/packages/conan"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	conan_module "code.gitea.io/gitea/modules/packages/conan"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	notify_service "code.gitea.io/gitea/services/notify"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
diff --git a/routers/api/packages/conan/search.go b/routers/api/packages/conan/search.go
index 2bcf9df162..7370c702cd 100644
--- a/routers/api/packages/conan/search.go
+++ b/routers/api/packages/conan/search.go
@@ -9,9 +9,9 @@ import (
 
 	conan_model "code.gitea.io/gitea/models/packages/conan"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	conan_module "code.gitea.io/gitea/modules/packages/conan"
+	"code.gitea.io/gitea/services/context"
 )
 
 // SearchResult contains the found recipe names
diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go
index 0bee7baa96..30c80fc15e 100644
--- a/routers/api/packages/conda/conda.go
+++ b/routers/api/packages/conda/conda.go
@@ -12,13 +12,13 @@ import (
 
 	packages_model "code.gitea.io/gitea/models/packages"
 	conda_model "code.gitea.io/gitea/models/packages/conda"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	conda_module "code.gitea.io/gitea/modules/packages/conda"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 
 	"github.com/dsnet/compress/bzip2"
diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go
index 8621242da4..e519766142 100644
--- a/routers/api/packages/container/container.go
+++ b/routers/api/packages/container/container.go
@@ -17,7 +17,6 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	container_model "code.gitea.io/gitea/models/packages/container"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
@@ -25,6 +24,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 	container_service "code.gitea.io/gitea/services/packages/container"
 
diff --git a/routers/api/packages/cran/cran.go b/routers/api/packages/cran/cran.go
index ae43df7c9a..2cec75294f 100644
--- a/routers/api/packages/cran/cran.go
+++ b/routers/api/packages/cran/cran.go
@@ -13,11 +13,11 @@ import (
 
 	packages_model "code.gitea.io/gitea/models/packages"
 	cran_model "code.gitea.io/gitea/models/packages/cran"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	cran_module "code.gitea.io/gitea/modules/packages/cran"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go
index 379137e87e..241de3ac5d 100644
--- a/routers/api/packages/debian/debian.go
+++ b/routers/api/packages/debian/debian.go
@@ -13,11 +13,11 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	debian_module "code.gitea.io/gitea/modules/packages/debian"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	notify_service "code.gitea.io/gitea/services/notify"
 	packages_service "code.gitea.io/gitea/services/packages"
 	debian_service "code.gitea.io/gitea/services/packages/debian"
diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go
index 30854335c0..8232931134 100644
--- a/routers/api/packages/generic/generic.go
+++ b/routers/api/packages/generic/generic.go
@@ -8,18 +8,19 @@ import (
 	"net/http"
 	"regexp"
 	"strings"
+	"unicode"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
 var (
-	packageNameRegex = regexp.MustCompile(`\A[A-Za-z0-9\.\_\-\+]+\z`)
-	filenameRegex    = packageNameRegex
+	packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
+	filenameRegex    = regexp.MustCompile(`\A[-_+=:;.()\[\]{}~!@#$%^& \w]+\z`)
 )
 
 func apiError(ctx *context.Context, status int, obj any) {
@@ -54,20 +55,38 @@ func DownloadPackageFile(ctx *context.Context) {
 	helper.ServePackageFile(ctx, s, u, pf)
 }
 
+func isValidPackageName(packageName string) bool {
+	if len(packageName) == 1 && !unicode.IsLetter(rune(packageName[0])) && !unicode.IsNumber(rune(packageName[0])) {
+		return false
+	}
+	return packageNameRegex.MatchString(packageName) && packageName != ".."
+}
+
+func isValidFileName(filename string) bool {
+	return filenameRegex.MatchString(filename) &&
+		strings.TrimSpace(filename) == filename &&
+		filename != "." && filename != ".."
+}
+
 // UploadPackage uploads the specific generic package.
 // Duplicated packages get rejected.
 func UploadPackage(ctx *context.Context) {
 	packageName := ctx.Params("packagename")
 	filename := ctx.Params("filename")
 
-	if !packageNameRegex.MatchString(packageName) || !filenameRegex.MatchString(filename) {
-		apiError(ctx, http.StatusBadRequest, errors.New("Invalid package name or filename"))
+	if !isValidPackageName(packageName) {
+		apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
+		return
+	}
+
+	if !isValidFileName(filename) {
+		apiError(ctx, http.StatusBadRequest, errors.New("invalid filename"))
 		return
 	}
 
 	packageVersion := ctx.Params("packageversion")
 	if packageVersion != strings.TrimSpace(packageVersion) {
-		apiError(ctx, http.StatusBadRequest, errors.New("Invalid package version"))
+		apiError(ctx, http.StatusBadRequest, errors.New("invalid package version"))
 		return
 	}
 
diff --git a/routers/api/packages/generic/generic_test.go b/routers/api/packages/generic/generic_test.go
new file mode 100644
index 0000000000..1acaafe576
--- /dev/null
+++ b/routers/api/packages/generic/generic_test.go
@@ -0,0 +1,65 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package generic
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidatePackageName(t *testing.T) {
+	bad := []string{
+		"",
+		".",
+		"..",
+		"-",
+		"a?b",
+		"a b",
+		"a/b",
+	}
+	for _, name := range bad {
+		assert.False(t, isValidPackageName(name), "bad=%q", name)
+	}
+
+	good := []string{
+		"a",
+		"1",
+		"a-",
+		"a_b",
+		"c.d+",
+	}
+	for _, name := range good {
+		assert.True(t, isValidPackageName(name), "good=%q", name)
+	}
+}
+
+func TestValidateFileName(t *testing.T) {
+	bad := []string{
+		"",
+		".",
+		"..",
+		"a?b",
+		"a/b",
+		" a",
+		"a ",
+	}
+	for _, name := range bad {
+		assert.False(t, isValidFileName(name), "bad=%q", name)
+	}
+
+	good := []string{
+		"-",
+		"a",
+		"1",
+		"a-",
+		"a_b",
+		"a b",
+		"c.d+",
+		`-_+=:;.()[]{}~!@#$%^& aA1`,
+	}
+	for _, name := range good {
+		assert.True(t, isValidFileName(name), "good=%q", name)
+	}
+}
diff --git a/routers/api/packages/goproxy/goproxy.go b/routers/api/packages/goproxy/goproxy.go
index 18e0074ab4..d658066bb4 100644
--- a/routers/api/packages/goproxy/goproxy.go
+++ b/routers/api/packages/goproxy/goproxy.go
@@ -12,11 +12,12 @@ import (
 	"time"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	goproxy_module "code.gitea.io/gitea/modules/packages/goproxy"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
@@ -129,7 +130,7 @@ func resolvePackage(ctx *context.Context, ownerID int64, name, version string) (
 				Value:      name,
 				ExactMatch: true,
 			},
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 			Sort:       packages_model.SortCreatedDesc,
 		})
 		if err != nil {
diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go
index a8daa69dc3..efdb83ec0e 100644
--- a/routers/api/packages/helm/helm.go
+++ b/routers/api/packages/helm/helm.go
@@ -13,14 +13,15 @@ import (
 	"time"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	helm_module "code.gitea.io/gitea/modules/packages/helm"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 
 	"gopkg.in/yaml.v3"
@@ -42,7 +43,7 @@ func Index(ctx *context.Context) {
 	pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages_model.TypeHelm,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
@@ -110,7 +111,7 @@ func DownloadPackageFile(ctx *context.Context) {
 			Value:      ctx.Params("package"),
 		},
 		HasFileWithName: filename,
-		IsInternal:      util.OptionalBoolFalse,
+		IsInternal:      optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
diff --git a/routers/api/packages/helper/helper.go b/routers/api/packages/helper/helper.go
index aadb10376c..cdb64109ad 100644
--- a/routers/api/packages/helper/helper.go
+++ b/routers/api/packages/helper/helper.go
@@ -10,9 +10,9 @@ import (
 	"net/url"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 // LogAndProcessError logs an error and calls a custom callback with the processed error message.
diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index 0b93382b01..27f0578db7 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -6,6 +6,7 @@ package maven
 import (
 	"crypto/md5"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/sha512"
 	"encoding/hex"
 	"encoding/xml"
@@ -19,15 +20,13 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	maven_module "code.gitea.io/gitea/modules/packages/maven"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
-
-	"github.com/minio/sha256-simd"
 )
 
 const (
diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go
index 8470874884..f8e839c424 100644
--- a/routers/api/packages/npm/api.go
+++ b/routers/api/packages/npm/api.go
@@ -12,6 +12,7 @@ import (
 
 	packages_model "code.gitea.io/gitea/models/packages"
 	npm_module "code.gitea.io/gitea/modules/packages/npm"
+	"code.gitea.io/gitea/modules/setting"
 )
 
 func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *npm_module.PackageMetadata {
@@ -98,7 +99,7 @@ func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total
 				Maintainers: []npm_module.User{}, // npm cli needs this field
 				Keywords:    metadata.Keywords,
 				Links: &npm_module.PackageSearchPackageLinks{
-					Registry: pd.FullWebLink(),
+					Registry: setting.AppURL + "api/packages/" + pd.Owner.Name + "/npm",
 					Homepage: metadata.ProjectURL,
 				},
 			},
diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go
index 170edfbe11..84acfffae2 100644
--- a/routers/api/packages/npm/npm.go
+++ b/routers/api/packages/npm/npm.go
@@ -17,12 +17,13 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	npm_module "code.gitea.io/gitea/modules/packages/npm"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 
 	"github.com/hashicorp/go-version"
@@ -120,7 +121,7 @@ func DownloadPackageFileByName(ctx *context.Context) {
 			Value:      packageNameFromParams(ctx),
 		},
 		HasFileWithName: filename,
-		IsInternal:      util.OptionalBoolFalse,
+		IsInternal:      optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
@@ -395,7 +396,7 @@ func setPackageTag(ctx std_ctx.Context, tag string, pv *packages_model.PackageVe
 			Properties: map[string]string{
 				npm_module.TagProperty: tag,
 			},
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 		})
 		if err != nil {
 			return err
@@ -431,7 +432,7 @@ func PackageSearch(ctx *context.Context) {
 	pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages_model.TypeNpm,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Name: packages_model.SearchValue{
 			ExactMatch: false,
 			Value:      ctx.FormTrim("text"),
diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go
index 769c4c1824..c28bc6c9d9 100644
--- a/routers/api/packages/nuget/nuget.go
+++ b/routers/api/packages/nuget/nuget.go
@@ -17,13 +17,14 @@ import (
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
 	nuget_model "code.gitea.io/gitea/models/packages/nuget"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	nuget_module "code.gitea.io/gitea/modules/packages/nuget"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
@@ -122,7 +123,7 @@ func SearchServiceV2(ctx *context.Context) {
 		Name: packages_model.SearchValue{
 			Value: getSearchTerm(ctx),
 		},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator:  paginator,
 	})
 	if err != nil {
@@ -172,7 +173,7 @@ func SearchServiceV2Count(ctx *context.Context) {
 		Name: packages_model.SearchValue{
 			Value: getSearchTerm(ctx),
 		},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
@@ -187,7 +188,7 @@ func SearchServiceV3(ctx *context.Context) {
 	pvs, count, err := nuget_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 		OwnerID:    ctx.Package.Owner.ID,
 		Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator: db.NewAbsoluteListOptions(
 			ctx.FormInt("skip"),
 			ctx.FormInt("take"),
@@ -313,7 +314,7 @@ func EnumeratePackageVersionsV2(ctx *context.Context) {
 			ExactMatch: true,
 			Value:      packageName,
 		},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator:  paginator,
 	})
 	if err != nil {
@@ -358,7 +359,7 @@ func EnumeratePackageVersionsV2Count(ctx *context.Context) {
 			ExactMatch: true,
 			Value:      strings.Trim(ctx.FormTrim("id"), "'"),
 		},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
diff --git a/routers/api/packages/pub/pub.go b/routers/api/packages/pub/pub.go
index 1f605c6c9f..f87df52a29 100644
--- a/routers/api/packages/pub/pub.go
+++ b/routers/api/packages/pub/pub.go
@@ -14,7 +14,6 @@ import (
 	"time"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
@@ -22,6 +21,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go
index 5718b1203b..7824db1823 100644
--- a/routers/api/packages/pypi/pypi.go
+++ b/routers/api/packages/pypi/pypi.go
@@ -12,12 +12,12 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	pypi_module "code.gitea.io/gitea/modules/packages/pypi"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go
index 5d06680552..4de361c214 100644
--- a/routers/api/packages/rpm/rpm.go
+++ b/routers/api/packages/rpm/rpm.go
@@ -13,13 +13,13 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	rpm_module "code.gitea.io/gitea/modules/packages/rpm"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	notify_service "code.gitea.io/gitea/services/notify"
 	packages_service "code.gitea.io/gitea/services/packages"
 	rpm_service "code.gitea.io/gitea/services/packages/rpm"
diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go
index 01fd4dad66..d2fbcd01f0 100644
--- a/routers/api/packages/rubygems/rubygems.go
+++ b/routers/api/packages/rubygems/rubygems.go
@@ -13,11 +13,12 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
@@ -43,7 +44,7 @@ func EnumeratePackagesLatest(ctx *context.Context) {
 	pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages_model.TypeRubyGems,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
@@ -304,7 +305,7 @@ func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_m
 		OwnerID:         ctx.Package.Owner.ID,
 		Type:            packages_model.TypeRubyGems,
 		HasFileWithName: filename,
-		IsInternal:      util.OptionalBoolFalse,
+		IsInternal:      optional.Some(false),
 	})
 	return pvs, err
 }
diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go
index 427e262d06..a9da3ea9c2 100644
--- a/routers/api/packages/swift/swift.go
+++ b/routers/api/packages/swift/swift.go
@@ -13,14 +13,15 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	swift_module "code.gitea.io/gitea/modules/packages/swift"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 
 	"github.com/hashicorp/go-version"
@@ -157,7 +158,7 @@ func EnumeratePackageVersions(ctx *context.Context) {
 }
 
 type Resource struct {
-	Name     string `json:"id"`
+	Name     string `json:"name"`
 	Type     string `json:"type"`
 	Checksum string `json:"checksum"`
 }
@@ -433,7 +434,7 @@ func LookupPackageIdentifiers(ctx *context.Context) {
 		Properties: map[string]string{
 			swift_module.PropertyRepositoryURL: url,
 		},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go
index af9cd08a62..98a81da368 100644
--- a/routers/api/packages/vagrant/vagrant.go
+++ b/routers/api/packages/vagrant/vagrant.go
@@ -12,11 +12,11 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	vagrant_module "code.gitea.io/gitea/modules/packages/vagrant"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 
 	"github.com/hashicorp/go-version"
diff --git a/routers/api/v1/activitypub/person.go b/routers/api/v1/activitypub/person.go
index cad5032d10..995a148f0b 100644
--- a/routers/api/v1/activitypub/person.go
+++ b/routers/api/v1/activitypub/person.go
@@ -9,9 +9,9 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/activitypub"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 
 	ap "github.com/go-ap/activitypub"
 	"github.com/go-ap/jsonld"
diff --git a/routers/api/v1/activitypub/reqsignature.go b/routers/api/v1/activitypub/reqsignature.go
index 3f60ed7776..59ebc74b89 100644
--- a/routers/api/v1/activitypub/reqsignature.go
+++ b/routers/api/v1/activitypub/reqsignature.go
@@ -13,9 +13,9 @@ import (
 	"net/url"
 
 	"code.gitea.io/gitea/modules/activitypub"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/setting"
+	gitea_context "code.gitea.io/gitea/services/context"
 
 	ap "github.com/go-ap/activitypub"
 	"github.com/go-fed/httpsig"
diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go
index bf030eb222..a4708fe032 100644
--- a/routers/api/v1/admin/adopt.go
+++ b/routers/api/v1/admin/adopt.go
@@ -8,9 +8,9 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/api/v1/admin/cron.go b/routers/api/v1/admin/cron.go
index cc8c6c9e23..e1ca6048c9 100644
--- a/routers/api/v1/admin/cron.go
+++ b/routers/api/v1/admin/cron.go
@@ -6,11 +6,11 @@ package admin
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/cron"
 )
 
diff --git a/routers/api/v1/admin/email.go b/routers/api/v1/admin/email.go
index 5914215bc2..ba963e9f69 100644
--- a/routers/api/v1/admin/email.go
+++ b/routers/api/v1/admin/email.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go
index 8a095a7def..4c168b55bf 100644
--- a/routers/api/v1/admin/hooks.go
+++ b/routers/api/v1/admin/hooks.go
@@ -8,12 +8,13 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/models/webhook"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	webhook_service "code.gitea.io/gitea/services/webhook"
 )
 
@@ -37,7 +38,7 @@ func ListHooks(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/HookList"
 
-	sysHooks, err := webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
+	sysHooks, err := webhook.GetSystemWebhooks(ctx, optional.None[bool]())
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err)
 		return
diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go
index bf68942a9c..a5c299bbf0 100644
--- a/routers/api/v1/admin/org.go
+++ b/routers/api/v1/admin/org.go
@@ -10,10 +10,10 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/admin/repo.go b/routers/api/v1/admin/repo.go
index a4895f260b..c119d5390a 100644
--- a/routers/api/v1/admin/repo.go
+++ b/routers/api/v1/admin/repo.go
@@ -4,10 +4,10 @@
 package admin
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/repo"
+	"code.gitea.io/gitea/services/context"
 )
 
 // CreateRepo api for creating a repository
diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go
index c0d9364435..329242d9f6 100644
--- a/routers/api/v1/admin/runners.go
+++ b/routers/api/v1/admin/runners.go
@@ -4,8 +4,8 @@
 package admin
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/shared"
+	"code.gitea.io/gitea/services/context"
 )
 
 // https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 272996f43d..87a5b28fad 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -15,17 +15,16 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/user"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/mailer"
 	user_service "code.gitea.io/gitea/services/user"
@@ -117,11 +116,8 @@ func CreateUser(ctx *context.APIContext) {
 	}
 
 	overwriteDefault := &user_model.CreateUserOverwriteOptions{
-		IsActive: util.OptionalBoolTrue,
-	}
-
-	if form.Restricted != nil {
-		overwriteDefault.IsRestricted = util.OptionalBoolOf(*form.Restricted)
+		IsActive:     optional.Some(true),
+		IsRestricted: optional.FromPtr(form.Restricted),
 	}
 
 	if form.Visibility != "" {
@@ -137,7 +133,7 @@ func CreateUser(ctx *context.APIContext) {
 		u.UpdatedUnix = u.CreatedUnix
 	}
 
-	if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
+	if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil {
 		if user_model.IsErrUserAlreadyExist(err) ||
 			user_model.IsErrEmailAlreadyUsed(err) ||
 			db.IsErrNameReserved(err) ||
@@ -151,6 +147,11 @@ func CreateUser(ctx *context.APIContext) {
 		}
 		return
 	}
+
+	if !user_model.IsEmailDomainAllowed(u.Email) {
+		ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email))
+	}
+
 	log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
 
 	// Send email notification.
@@ -213,7 +214,7 @@ func EditUser(ctx *context.APIContext) {
 	}
 
 	if form.Email != nil {
-		if err := user_service.AddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
+		if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
 			switch {
 			case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
 				ctx.Error(http.StatusBadRequest, "EmailInvalid", err)
@@ -224,6 +225,10 @@ func EditUser(ctx *context.APIContext) {
 			}
 			return
 		}
+
+		if !user_model.IsEmailDomainAllowed(*form.Email) {
+			ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email))
+		}
 	}
 
 	opts := &user_service.UpdateOptions{
diff --git a/routers/api/v1/admin/user_badge.go b/routers/api/v1/admin/user_badge.go
new file mode 100644
index 0000000000..bacd1f809b
--- /dev/null
+++ b/routers/api/v1/admin/user_badge.go
@@ -0,0 +1,124 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+	"net/http"
+
+	user_model "code.gitea.io/gitea/models/user"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
+)
+
+// ListUserBadges lists all badges belonging to a user
+func ListUserBadges(ctx *context.APIContext) {
+	// swagger:operation GET /admin/users/{username}/badges admin adminListUserBadges
+	// ---
+	// summary: List a user's badges
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: username of user
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/BadgeList"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	badges, maxResults, err := user_model.GetUserBadges(ctx, ctx.ContextUser)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetUserBadges", err)
+		return
+	}
+
+	ctx.SetTotalCountHeader(maxResults)
+	ctx.JSON(http.StatusOK, &badges)
+}
+
+// AddUserBadges add badges to a user
+func AddUserBadges(ctx *context.APIContext) {
+	// swagger:operation POST /admin/users/{username}/badges admin adminAddUserBadges
+	// ---
+	// summary: Add a badge to a user
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: username of user
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/UserBadgeOption"
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
+
+	form := web.GetForm(ctx).(*api.UserBadgeOption)
+	badges := prepareBadgesForReplaceOrAdd(ctx, *form)
+
+	if err := user_model.AddUserBadges(ctx, ctx.ContextUser, badges); err != nil {
+		ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// DeleteUserBadges delete a badge from a user
+func DeleteUserBadges(ctx *context.APIContext) {
+	// swagger:operation DELETE /admin/users/{username}/badges admin adminDeleteUserBadges
+	// ---
+	// summary: Remove a badge from a user
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: username of user
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/UserBadgeOption"
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+
+	form := web.GetForm(ctx).(*api.UserBadgeOption)
+	badges := prepareBadgesForReplaceOrAdd(ctx, *form)
+
+	if err := user_model.RemoveUserBadges(ctx, ctx.ContextUser, badges); err != nil {
+		ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+func prepareBadgesForReplaceOrAdd(ctx *context.APIContext, form api.UserBadgeOption) []*user_model.Badge {
+	badges := make([]*user_model.Badge, len(form.BadgeSlugs))
+	for i, badge := range form.BadgeSlugs {
+		badges[i] = &user_model.Badge{
+			Slug: badge,
+		}
+	}
+	return badges
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index cb1803f7c6..e870378c4b 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -6,9 +6,9 @@
 //
 // This documentation describes the Gitea API.
 //
-//	Schemes: http, https
+//	Schemes: https, http
 //	BasePath: /api/v1
-//	Version: {{AppVer | JSEscape | Safe}}
+//	Version: {{AppVer | JSEscape}}
 //	License: MIT http://opensource.org/licenses/MIT
 //
 //	Consumes:
@@ -79,7 +79,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
@@ -95,7 +94,7 @@ import (
 	"code.gitea.io/gitea/routers/api/v1/user"
 	"code.gitea.io/gitea/routers/common"
 	"code.gitea.io/gitea/services/auth"
-	context_service "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 
 	_ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation
@@ -811,7 +810,7 @@ func individualPermsChecker(ctx *context.APIContext) {
 // check for and warn against deprecated authentication options
 func checkDeprecatedAuthMethods(ctx *context.APIContext) {
 	if ctx.FormString("token") != "" || ctx.FormString("access_token") != "" {
-		ctx.Resp.Header().Set("Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.")
+		ctx.Resp.Header().Set("X-Gitea-Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.")
 	}
 }
 
@@ -855,11 +854,11 @@ func Routes() *web.Route {
 				m.Group("/user/{username}", func() {
 					m.Get("", activitypub.Person)
 					m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
-				}, context_service.UserAssignmentAPI())
+				}, context.UserAssignmentAPI())
 				m.Group("/user-id/{user-id}", func() {
 					m.Get("", activitypub.Person)
 					m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
-				}, context_service.UserIDAssignmentAPI())
+				}, context.UserIDAssignmentAPI())
 			}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
 		}
 
@@ -915,7 +914,7 @@ func Routes() *web.Route {
 				}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
 
 				m.Get("/activities/feeds", user.ListUserActivityFeeds)
-			}, context_service.UserAssignmentAPI(), individualPermsChecker)
+			}, context.UserAssignmentAPI(), individualPermsChecker)
 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
 
 		// Users (requires user scope)
@@ -933,7 +932,7 @@ func Routes() *web.Route {
 				m.Get("/starred", user.GetStarredRepos)
 
 				m.Get("/subscriptions", user.GetWatchedRepos)
-			}, context_service.UserAssignmentAPI())
+			}, context.UserAssignmentAPI())
 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
 
 		// Users (requires user scope)
@@ -956,6 +955,15 @@ func Routes() *web.Route {
 						Delete(user.DeleteSecret)
 				})
 
+				m.Group("/variables", func() {
+					m.Get("", user.ListVariables)
+					m.Combo("/{variablename}").
+						Get(user.GetVariable).
+						Delete(user.DeleteVariable).
+						Post(bind(api.CreateVariableOption{}), user.CreateVariable).
+						Put(bind(api.UpdateVariableOption{}), user.UpdateVariable)
+				})
+
 				m.Group("/runners", func() {
 					m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
 				})
@@ -968,7 +976,7 @@ func Routes() *web.Route {
 					m.Get("", user.CheckMyFollowing)
 					m.Put("", user.Follow)
 					m.Delete("", user.Unfollow)
-				}, context_service.UserAssignmentAPI())
+				}, context.UserAssignmentAPI())
 			})
 
 			// (admin:public_key scope)
@@ -1028,7 +1036,16 @@ func Routes() *web.Route {
 			m.Group("/avatar", func() {
 				m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
 				m.Delete("", user.DeleteAvatar)
-			}, reqToken())
+			})
+
+			m.Group("/blocks", func() {
+				m.Get("", user.ListBlocks)
+				m.Group("/{username}", func() {
+					m.Get("", user.CheckUserBlock)
+					m.Put("", user.BlockUser)
+					m.Delete("", user.UnblockUser)
+				}, context.UserAssignmentAPI())
+			})
 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
 
 		// Repositories (requires repo scope, org scope)
@@ -1065,6 +1082,15 @@ func Routes() *web.Route {
 							Delete(reqToken(), reqOwner(), repo.DeleteSecret)
 					})
 
+					m.Group("/variables", func() {
+						m.Get("", reqToken(), reqOwner(), repo.ListVariables)
+						m.Combo("/{variablename}").
+							Get(reqToken(), reqOwner(), repo.GetVariable).
+							Delete(reqToken(), reqOwner(), repo.DeleteVariable).
+							Post(reqToken(), reqOwner(), bind(api.CreateVariableOption{}), repo.CreateVariable).
+							Put(reqToken(), reqOwner(), bind(api.UpdateVariableOption{}), repo.UpdateVariable)
+					})
+
 					m.Group("/runners", func() {
 						m.Get("/registration-token", reqToken(), reqOwner(), repo.GetRegistrationToken)
 					})
@@ -1225,6 +1251,7 @@ func Routes() *web.Route {
 							Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
 							Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
 					})
+					m.Get("/{base}/*", repo.GetPullRequestByBaseHead)
 				}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
 				m.Group("/statuses", func() {
 					m.Combo("/{sha}").Get(repo.GetCommitStatuses).
@@ -1235,6 +1262,7 @@ func Routes() *web.Route {
 					m.Group("/{ref}", func() {
 						m.Get("/status", repo.GetCombinedCommitStatusByRef)
 						m.Get("/statuses", repo.GetCommitStatusesByRef)
+						m.Get("/pull", repo.GetCommitPullRequest)
 					}, context.ReferencesGitRepo())
 				}, reqRepoReader(unit.TypeCode))
 				m.Group("/git", func() {
@@ -1413,14 +1441,14 @@ func Routes() *web.Route {
 				m.Get("/files", reqToken(), packages.ListPackageFiles)
 			})
 			m.Get("/", reqToken(), packages.ListPackages)
-		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
 
 		// Organizations
 		m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
 		m.Group("/users/{username}/orgs", func() {
 			m.Get("", reqToken(), org.ListUserOrgs)
 			m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
-		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI())
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI())
 		m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
 		m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
 		m.Group("/orgs/{org}", func() {
@@ -1442,6 +1470,15 @@ func Routes() *web.Route {
 						Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret)
 				})
 
+				m.Group("/variables", func() {
+					m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables)
+					m.Combo("/{variablename}").
+						Get(reqToken(), reqOrgOwnership(), org.GetVariable).
+						Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable).
+						Post(reqToken(), reqOrgOwnership(), bind(api.CreateVariableOption{}), org.CreateVariable).
+						Put(reqToken(), reqOrgOwnership(), bind(api.UpdateVariableOption{}), org.UpdateVariable)
+				})
+
 				m.Group("/runners", func() {
 					m.Get("/registration-token", reqToken(), reqOrgOwnership(), org.GetRegistrationToken)
 				})
@@ -1476,6 +1513,15 @@ func Routes() *web.Route {
 				m.Delete("", org.DeleteAvatar)
 			}, reqToken(), reqOrgOwnership())
 			m.Get("/activities/feeds", org.ListOrgActivityFeeds)
+
+			m.Group("/blocks", func() {
+				m.Get("", org.ListBlocks)
+				m.Group("/{username}", func() {
+					m.Get("", org.CheckUserBlock)
+					m.Put("", org.BlockUser)
+					m.Delete("", org.UnblockUser)
+				})
+			}, reqToken(), reqOrgOwnership())
 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
 		m.Group("/teams/{teamid}", func() {
 			m.Combo("").Get(reqToken(), org.GetTeam).
@@ -1518,7 +1564,10 @@ func Routes() *web.Route {
 					m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
 					m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
 					m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
-				}, context_service.UserAssignmentAPI())
+					m.Get("/badges", admin.ListUserBadges)
+					m.Post("/badges", bind(api.UserBadgeOption{}), admin.AddUserBadges)
+					m.Delete("/badges", bind(api.UserBadgeOption{}), admin.DeleteUserBadges)
+				}, context.UserAssignmentAPI())
 			})
 			m.Group("/emails", func() {
 				m.Get("", admin.GetAllEmails)
diff --git a/routers/api/v1/misc/gitignore.go b/routers/api/v1/misc/gitignore.go
index 7c7fe4b125..dffd771752 100644
--- a/routers/api/v1/misc/gitignore.go
+++ b/routers/api/v1/misc/gitignore.go
@@ -6,11 +6,11 @@ package misc
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/options"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Shows a list of all Gitignore templates
diff --git a/routers/api/v1/misc/label_templates.go b/routers/api/v1/misc/label_templates.go
index 0e0ca39fc5..cc11f37626 100644
--- a/routers/api/v1/misc/label_templates.go
+++ b/routers/api/v1/misc/label_templates.go
@@ -6,9 +6,9 @@ package misc
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/misc/licenses.go b/routers/api/v1/misc/licenses.go
index 65f63468cf..2a980f5084 100644
--- a/routers/api/v1/misc/licenses.go
+++ b/routers/api/v1/misc/licenses.go
@@ -8,12 +8,12 @@ import (
 	"net/http"
 	"net/url"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/options"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Returns a list of all License templates
diff --git a/routers/api/v1/misc/markup.go b/routers/api/v1/misc/markup.go
index 7b24b353b6..9699c79368 100644
--- a/routers/api/v1/misc/markup.go
+++ b/routers/api/v1/misc/markup.go
@@ -6,12 +6,12 @@ package misc
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/common"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Markup render markup document to HTML
diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go
index ec8f8f47b7..5236fd06ae 100644
--- a/routers/api/v1/misc/markup_test.go
+++ b/routers/api/v1/misc/markup_test.go
@@ -10,19 +10,19 @@ import (
 	"strings"
 	"testing"
 
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
 
 const (
-	AppURL    = "http://localhost:3000/"
-	Repo      = "gogits/gogs"
-	AppSubURL = AppURL + Repo + "/"
+	AppURL  = "http://localhost:3000/"
+	Repo    = "gogits/gogs"
+	FullURL = AppURL + Repo + "/"
 )
 
 func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) {
@@ -74,20 +74,20 @@ func TestAPI_RenderGFM(t *testing.T) {
 		// rendered
 		`<p>Wiki! Enjoy :)</p>
 <ul>
-<li><a href="` + AppSubURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
-<li><a href="` + AppSubURL + `wiki/Tips" rel="nofollow">Tips</a></li>
+<li><a href="` + FullURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
+<li><a href="` + FullURL + `wiki/Tips" rel="nofollow">Tips</a></li>
 <li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
 </ul>
 `,
 		// Guard wiki sidebar: special syntax
 		`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
 		// rendered
-		`<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
+		`<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
 `,
 		// special syntax
 		`[[Name|Link]]`,
 		// rendered
-		`<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p>
+		`<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p>
 `,
 		// empty
 		``,
@@ -111,8 +111,8 @@ Here are some links to the most important topics. You can find the full list of
 <p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
 <h2 id="user-content-quick-links">Quick Links</h2>
 <p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
-<p><a href="` + AppSubURL + `wiki/Configuration" rel="nofollow">Configuration</a>
-<a href="` + AppSubURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + AppSubURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
+<p><a href="` + FullURL + `wiki/Configuration" rel="nofollow">Configuration</a>
+<a href="` + FullURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + FullURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
 `,
 	}
 
diff --git a/routers/api/v1/misc/nodeinfo.go b/routers/api/v1/misc/nodeinfo.go
index cc754f64a2..3bd80de5c1 100644
--- a/routers/api/v1/misc/nodeinfo.go
+++ b/routers/api/v1/misc/nodeinfo.go
@@ -9,9 +9,9 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 )
 
 const cacheKeyNodeInfoUsage = "API_NodeInfoUsage"
diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go
index 2ca9813e15..24a46c1e70 100644
--- a/routers/api/v1/misc/signing.go
+++ b/routers/api/v1/misc/signing.go
@@ -7,8 +7,8 @@ import (
 	"fmt"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 )
 
 // SigningKey returns the public key of the default signing key if it exists
diff --git a/routers/api/v1/misc/version.go b/routers/api/v1/misc/version.go
index 83fa35219a..e3b43a0e6b 100644
--- a/routers/api/v1/misc/version.go
+++ b/routers/api/v1/misc/version.go
@@ -6,9 +6,9 @@ package misc
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Version shows the version of the Gitea server
diff --git a/routers/api/v1/notify/notifications.go b/routers/api/v1/notify/notifications.go
index c87da9399f..46b3c7f5e7 100644
--- a/routers/api/v1/notify/notifications.go
+++ b/routers/api/v1/notify/notifications.go
@@ -9,9 +9,9 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 )
 
 // NewAvailable check if unread notifications exist
diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go
index 55ca6ad1fd..1744426ee8 100644
--- a/routers/api/v1/notify/repo.go
+++ b/routers/api/v1/notify/repo.go
@@ -10,9 +10,8 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
-	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -201,7 +200,6 @@ func ReadRepoNotifications(ctx *context.APIContext) {
 	if !ctx.FormBool("all") {
 		statuses := ctx.FormStrings("status-types")
 		opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
-		log.Error("%v", opts.Status)
 	}
 	nl, err := db.Find[activities_model.Notification](ctx, opts)
 	if err != nil {
diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go
index 919e52952d..8e12d359cb 100644
--- a/routers/api/v1/notify/threads.go
+++ b/routers/api/v1/notify/threads.go
@@ -10,7 +10,7 @@ import (
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go
index 4abdfb2e92..879f484cce 100644
--- a/routers/api/v1/notify/user.go
+++ b/routers/api/v1/notify/user.go
@@ -9,8 +9,8 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/org/avatar.go b/routers/api/v1/org/avatar.go
index 7b621a50c3..e34c68dfc9 100644
--- a/routers/api/v1/org/avatar.go
+++ b/routers/api/v1/org/avatar.go
@@ -7,9 +7,9 @@ import (
 	"encoding/base64"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	user_service "code.gitea.io/gitea/services/user"
 )
 
diff --git a/routers/api/v1/org/block.go b/routers/api/v1/org/block.go
new file mode 100644
index 0000000000..69a5222a20
--- /dev/null
+++ b/routers/api/v1/org/block.go
@@ -0,0 +1,116 @@
+// Copyright 2024 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+	"code.gitea.io/gitea/routers/api/v1/shared"
+	"code.gitea.io/gitea/services/context"
+)
+
+func ListBlocks(ctx *context.APIContext) {
+	// swagger:operation GET /orgs/{org}/blocks organization organizationListBlocks
+	// ---
+	// summary: List users blocked by the organization
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results
+	//   type: integer
+	// produces:
+	// - application/json
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/UserList"
+
+	shared.ListBlocks(ctx, ctx.Org.Organization.AsUser())
+}
+
+func CheckUserBlock(ctx *context.APIContext) {
+	// swagger:operation GET /orgs/{org}/blocks/{username} organization organizationCheckUserBlock
+	// ---
+	// summary: Check if a user is blocked by the organization
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: username
+	//   in: path
+	//   description: user to check
+	//   type: string
+	//   required: true
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	shared.CheckUserBlock(ctx, ctx.Org.Organization.AsUser())
+}
+
+func BlockUser(ctx *context.APIContext) {
+	// swagger:operation PUT /orgs/{org}/blocks/{username} organization organizationBlockUser
+	// ---
+	// summary: Block a user
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: username
+	//   in: path
+	//   description: user to block
+	//   type: string
+	//   required: true
+	// - name: note
+	//   in: query
+	//   description: optional note for the block
+	//   type: string
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+
+	shared.BlockUser(ctx, ctx.Org.Organization.AsUser())
+}
+
+func UnblockUser(ctx *context.APIContext) {
+	// swagger:operation DELETE /orgs/{org}/blocks/{username} organization organizationUnblockUser
+	// ---
+	// summary: Unblock a user
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: username
+	//   in: path
+	//   description: user to unblock
+	//   type: string
+	//   required: true
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+
+	shared.UnblockUser(ctx, ctx.Doer, ctx.Org.Organization.AsUser())
+}
diff --git a/routers/api/v1/org/hook.go b/routers/api/v1/org/hook.go
index 3c3f058b5d..c1dc0519ea 100644
--- a/routers/api/v1/org/hook.go
+++ b/routers/api/v1/org/hook.go
@@ -6,10 +6,10 @@ package org
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	webhook_service "code.gitea.io/gitea/services/webhook"
 )
 
diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go
index 5a03059ded..b5ec54ccf4 100644
--- a/routers/api/v1/org/label.go
+++ b/routers/api/v1/org/label.go
@@ -9,11 +9,11 @@ import (
 	"strings"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/label"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go
index 422b7cecfe..9db9ad964b 100644
--- a/routers/api/v1/org/member.go
+++ b/routers/api/v1/org/member.go
@@ -9,11 +9,11 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/organization"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/user"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -318,7 +318,7 @@ func DeleteMember(ctx *context.APIContext) {
 	if ctx.Written() {
 		return
 	}
-	if err := models.RemoveOrgUser(ctx, ctx.Org.Organization.ID, member.ID); err != nil {
+	if err := models.RemoveOrgUser(ctx, ctx.Org.Organization, member); err != nil {
 		ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err)
 	}
 	ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go
index 255e28c706..e848d95181 100644
--- a/routers/api/v1/org/org.go
+++ b/routers/api/v1/org/org.go
@@ -12,12 +12,12 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/perm"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/user"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/org"
 	user_service "code.gitea.io/gitea/services/user"
diff --git a/routers/api/v1/org/runners.go b/routers/api/v1/org/runners.go
index 05bce8daef..2a52bd8778 100644
--- a/routers/api/v1/org/runners.go
+++ b/routers/api/v1/org/runners.go
@@ -4,8 +4,8 @@
 package org
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/shared"
+	"code.gitea.io/gitea/services/context"
 )
 
 // https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
diff --git a/routers/api/v1/org/secrets.go b/routers/api/v1/org/secrets.go
index ddc74d865b..abb6bb26c4 100644
--- a/routers/api/v1/org/secrets.go
+++ b/routers/api/v1/org/secrets.go
@@ -9,11 +9,11 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	secret_model "code.gitea.io/gitea/models/secret"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	secret_service "code.gitea.io/gitea/services/secrets"
 )
 
diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go
index f129c66230..015af774e3 100644
--- a/routers/api/v1/org/team.go
+++ b/routers/api/v1/org/team.go
@@ -15,12 +15,13 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/user"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	org_service "code.gitea.io/gitea/services/org"
 	repo_service "code.gitea.io/gitea/services/repository"
@@ -486,6 +487,8 @@ func AddTeamMember(ctx *context.APIContext) {
 	// responses:
 	//   "204":
 	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
@@ -493,8 +496,12 @@ func AddTeamMember(ctx *context.APIContext) {
 	if ctx.Written() {
 		return
 	}
-	if err := models.AddTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
-		ctx.Error(http.StatusInternalServerError, "AddMember", err)
+	if err := models.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "AddTeamMember", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "AddTeamMember", err)
+		}
 		return
 	}
 	ctx.Status(http.StatusNoContent)
@@ -530,7 +537,7 @@ func RemoveTeamMember(ctx *context.APIContext) {
 		return
 	}
 
-	if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
+	if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u); err != nil {
 		ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err)
 		return
 	}
diff --git a/routers/api/v1/org/variables.go b/routers/api/v1/org/variables.go
new file mode 100644
index 0000000000..eaf7bdc45b
--- /dev/null
+++ b/routers/api/v1/org/variables.go
@@ -0,0 +1,291 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+	"errors"
+	"net/http"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/routers/api/v1/utils"
+	actions_service "code.gitea.io/gitea/services/actions"
+	"code.gitea.io/gitea/services/context"
+)
+
+// ListVariables list org-level variables
+func ListVariables(ctx *context.APIContext) {
+	// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
+	// ---
+	// summary: Get an org-level variables list
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results
+	//   type: integer
+	// responses:
+	//   "200":
+	//		 "$ref": "#/responses/VariableList"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
+		OwnerID:     ctx.Org.Organization.ID,
+		ListOptions: utils.GetListOptions(ctx),
+	})
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "FindVariables", err)
+		return
+	}
+
+	variables := make([]*api.ActionVariable, len(vars))
+	for i, v := range vars {
+		variables[i] = &api.ActionVariable{
+			OwnerID: v.OwnerID,
+			RepoID:  v.RepoID,
+			Name:    v.Name,
+			Data:    v.Data,
+		}
+	}
+
+	ctx.SetTotalCountHeader(count)
+	ctx.JSON(http.StatusOK, variables)
+}
+
+// GetVariable get an org-level variable
+func GetVariable(ctx *context.APIContext) {
+	// swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable
+	// ---
+	// summary: Get an org-level variable
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//		 "$ref": "#/responses/ActionVariable"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ctx.Org.Organization.ID,
+		Name:    ctx.Params("variablename"),
+	})
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "GetVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		}
+		return
+	}
+
+	variable := &api.ActionVariable{
+		OwnerID: v.OwnerID,
+		RepoID:  v.RepoID,
+		Name:    v.Name,
+		Data:    v.Data,
+	}
+
+	ctx.JSON(http.StatusOK, variable)
+}
+
+// DeleteVariable delete an org-level variable
+func DeleteVariable(ctx *context.APIContext) {
+	// swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable
+	// ---
+	// summary: Delete an org-level variable
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//			"$ref": "#/responses/ActionVariable"
+	//   "201":
+	//     description: response when deleting a variable
+	//   "204":
+	//     description: response when deleting a variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("variablename")); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
+		} else if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// CreateVariable create an org-level variable
+func CreateVariable(ctx *context.APIContext) {
+	// swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable
+	// ---
+	// summary: Create an org-level variable
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/CreateVariableOption"
+	// responses:
+	//   "201":
+	//     description: response when creating an org-level variable
+	//   "204":
+	//     description: response when creating an org-level variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	opt := web.GetForm(ctx).(*api.CreateVariableOption)
+
+	ownerID := ctx.Org.Organization.ID
+	variableName := ctx.Params("variablename")
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ownerID,
+		Name:    variableName,
+	})
+	if err != nil && !errors.Is(err, util.ErrNotExist) {
+		ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		return
+	}
+	if v != nil && v.ID > 0 {
+		ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
+		return
+	}
+
+	if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "CreateVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// UpdateVariable update an org-level variable
+func UpdateVariable(ctx *context.APIContext) {
+	// swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable
+	// ---
+	// summary: Update an org-level variable
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/UpdateVariableOption"
+	// responses:
+	//   "201":
+	//     description: response when updating an org-level variable
+	//   "204":
+	//     description: response when updating an org-level variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	opt := web.GetForm(ctx).(*api.UpdateVariableOption)
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ctx.Org.Organization.ID,
+		Name:    ctx.Params("variablename"),
+	})
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "GetVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		}
+		return
+	}
+
+	if opt.Name == "" {
+		opt.Name = ctx.Params("variablename")
+	}
+	if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go
index a79ba315be..b38aa13167 100644
--- a/routers/api/v1/packages/package.go
+++ b/routers/api/v1/packages/package.go
@@ -7,10 +7,10 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
@@ -60,7 +60,7 @@ func ListPackages(ctx *context.APIContext) {
 		OwnerID:    ctx.Package.Owner.ID,
 		Type:       packages.Type(packageType),
 		Name:       packages.SearchValue{Value: query},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator:  &listOptions,
 	})
 	if err != nil {
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
index 039cdadac9..03321d956d 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -7,10 +7,14 @@ import (
 	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/routers/api/v1/utils"
+	actions_service "code.gitea.io/gitea/services/actions"
+	"code.gitea.io/gitea/services/context"
 	secret_service "code.gitea.io/gitea/services/secrets"
 )
 
@@ -127,3 +131,295 @@ func DeleteSecret(ctx *context.APIContext) {
 
 	ctx.Status(http.StatusNoContent)
 }
+
+// GetVariable get a repo-level variable
+func GetVariable(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable
+	// ---
+	// summary: Get a repo-level variable
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//			"$ref": "#/responses/ActionVariable"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		RepoID: ctx.Repo.Repository.ID,
+		Name:   ctx.Params("variablename"),
+	})
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "GetVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		}
+		return
+	}
+
+	variable := &api.ActionVariable{
+		OwnerID: v.OwnerID,
+		RepoID:  v.RepoID,
+		Name:    v.Name,
+		Data:    v.Data,
+	}
+
+	ctx.JSON(http.StatusOK, variable)
+}
+
+// DeleteVariable delete a repo-level variable
+func DeleteVariable(ctx *context.APIContext) {
+	// swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable
+	// ---
+	// summary: Delete a repo-level variable
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//			"$ref": "#/responses/ActionVariable"
+	//   "201":
+	//     description: response when deleting a variable
+	//   "204":
+	//     description: response when deleting a variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("variablename")); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
+		} else if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// CreateVariable create a repo-level variable
+func CreateVariable(ctx *context.APIContext) {
+	// swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable
+	// ---
+	// summary: Create a repo-level variable
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/CreateVariableOption"
+	// responses:
+	//   "201":
+	//     description: response when creating a repo-level variable
+	//   "204":
+	//     description: response when creating a repo-level variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	opt := web.GetForm(ctx).(*api.CreateVariableOption)
+
+	repoID := ctx.Repo.Repository.ID
+	variableName := ctx.Params("variablename")
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		RepoID: repoID,
+		Name:   variableName,
+	})
+	if err != nil && !errors.Is(err, util.ErrNotExist) {
+		ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		return
+	}
+	if v != nil && v.ID > 0 {
+		ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
+		return
+	}
+
+	if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "CreateVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// UpdateVariable update a repo-level variable
+func UpdateVariable(ctx *context.APIContext) {
+	// swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable
+	// ---
+	// summary: Update a repo-level variable
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/UpdateVariableOption"
+	// responses:
+	//   "201":
+	//     description: response when updating a repo-level variable
+	//   "204":
+	//     description: response when updating a repo-level variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	opt := web.GetForm(ctx).(*api.UpdateVariableOption)
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		RepoID: ctx.Repo.Repository.ID,
+		Name:   ctx.Params("variablename"),
+	})
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "GetVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		}
+		return
+	}
+
+	if opt.Name == "" {
+		opt.Name = ctx.Params("variablename")
+	}
+	if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// ListVariables list repo-level variables
+func ListVariables(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList
+	// ---
+	// summary: Get repo-level variables list
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results
+	//   type: integer
+	// responses:
+	//   "200":
+	//		 "$ref": "#/responses/VariableList"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
+		RepoID:      ctx.Repo.Repository.ID,
+		ListOptions: utils.GetListOptions(ctx),
+	})
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "FindVariables", err)
+		return
+	}
+
+	variables := make([]*api.ActionVariable, len(vars))
+	for i, v := range vars {
+		variables[i] = &api.ActionVariable{
+			OwnerID: v.OwnerID,
+			RepoID:  v.RepoID,
+			Name:    v.Name,
+		}
+	}
+
+	ctx.SetTotalCountHeader(count)
+	ctx.JSON(http.StatusOK, variables)
+}
diff --git a/routers/api/v1/repo/avatar.go b/routers/api/v1/repo/avatar.go
index 1b661955f0..698337ffd2 100644
--- a/routers/api/v1/repo/avatar.go
+++ b/routers/api/v1/repo/avatar.go
@@ -7,9 +7,9 @@ import (
 	"encoding/base64"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go
index 26605bba03..3b116666ea 100644
--- a/routers/api/v1/repo/blob.go
+++ b/routers/api/v1/repo/blob.go
@@ -6,7 +6,7 @@ package repo
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	files_service "code.gitea.io/gitea/services/repository/files"
 )
 
diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go
index 21e4b05a8b..1574cf046c 100644
--- a/routers/api/v1/repo/branch.go
+++ b/routers/api/v1/repo/branch.go
@@ -14,14 +14,14 @@ import (
 	git_model "code.gitea.io/gitea/models/git"
 	"code.gitea.io/gitea/models/organization"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	api "code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	pull_service "code.gitea.io/gitea/services/pull"
 	repo_service "code.gitea.io/gitea/services/repository"
@@ -141,7 +141,7 @@ func DeleteBranch(ctx *context.APIContext) {
 	// check whether branches of this repository has been synced
 	totalNumOfBranches, err := db.Count[git_model.Branch](ctx, git_model.FindBranchOptions{
 		RepoID:          ctx.Repo.Repository.ID,
-		IsDeletedBranch: util.OptionalBoolFalse,
+		IsDeletedBranch: optional.Some(false),
 	})
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "CountBranches", err)
@@ -340,7 +340,7 @@ func ListBranches(ctx *context.APIContext) {
 		branchOpts := git_model.FindBranchOptions{
 			ListOptions:     listOptions,
 			RepoID:          ctx.Repo.Repository.ID,
-			IsDeletedBranch: util.OptionalBoolFalse,
+			IsDeletedBranch: optional.Some(false),
 		}
 		var err error
 		totalNumOfBranches, err = db.Count[git_model.Branch](ctx, branchOpts)
diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go
index a222e50a5e..4ce14f7d01 100644
--- a/routers/api/v1/repo/collaborators.go
+++ b/routers/api/v1/repo/collaborators.go
@@ -8,16 +8,15 @@ import (
 	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
@@ -54,15 +53,10 @@ func ListCollaborators(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	count, err := db.Count[repo_model.Collaboration](ctx, repo_model.FindCollaborationOptions{
-		RepoID: ctx.Repo.Repository.ID,
+	collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{
+		ListOptions: utils.GetListOptions(ctx),
+		RepoID:      ctx.Repo.Repository.ID,
 	})
-	if err != nil {
-		ctx.InternalServerError(err)
-		return
-	}
-
-	collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
 		return
@@ -73,7 +67,7 @@ func ListCollaborators(ctx *context.APIContext) {
 		users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer)
 	}
 
-	ctx.SetTotalCountHeader(count)
+	ctx.SetTotalCountHeader(total)
 	ctx.JSON(http.StatusOK, users)
 }
 
@@ -159,6 +153,8 @@ func AddCollaborator(ctx *context.APIContext) {
 	// responses:
 	//   "204":
 	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 	//   "422":
@@ -182,7 +178,11 @@ func AddCollaborator(ctx *context.APIContext) {
 	}
 
 	if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil {
-		ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "AddCollaborator", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
+		}
 		return
 	}
 
@@ -237,7 +237,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
 		return
 	}
 
-	if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator.ID); err != nil {
+	if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
 		ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err)
 		return
 	}
diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go
index 43b6400009..d06a3b4e49 100644
--- a/routers/api/v1/repo/commits.go
+++ b/routers/api/v1/repo/commits.go
@@ -10,12 +10,13 @@ import (
 	"net/http"
 	"strconv"
 
+	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -323,3 +324,53 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) {
 		return
 	}
 }
+
+// GetCommitPullRequest returns the pull request of the commit
+func GetCommitPullRequest(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/pull repository repoGetCommitPullRequest
+	// ---
+	// summary: Get the pull request of the commit
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// - name: sha
+	//   in: path
+	//   description: SHA of the commit to get
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/PullRequest"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	pr, err := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, ctx.Params(":sha"))
+	if err != nil {
+		if issues_model.IsErrPullRequestNotExist(err) {
+			ctx.Error(http.StatusNotFound, "GetPullRequestByMergedCommit", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
+		}
+		return
+	}
+
+	if err = pr.LoadBaseRepo(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
+		return
+	}
+	if err = pr.LoadHeadRepo(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
+		return
+	}
+	ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
+}
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 065d6bf8b2..156033f58a 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -19,7 +19,6 @@ import (
 	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/httpcache"
@@ -30,6 +29,7 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/common"
+	"code.gitea.io/gitea/services/context"
 	archiver_service "code.gitea.io/gitea/services/repository/archiver"
 	files_service "code.gitea.io/gitea/services/repository/files"
 )
@@ -145,7 +145,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
 		return
 	}
 
-	// OK, now the blob is known to have at most 1024 bytes we can simply read this in in one go (This saves reading it twice)
+	// OK, now the blob is known to have at most 1024 bytes we can simply read this in one go (This saves reading it twice)
 	dataRc, err := blob.DataAsync()
 	if err != nil {
 		ctx.ServerError("DataAsync", err)
@@ -408,7 +408,7 @@ func canReadFiles(r *context.Repository) bool {
 	return r.Permission.CanRead(unit.TypeCode)
 }
 
-func base64Reader(s string) (io.Reader, error) {
+func base64Reader(s string) (io.ReadSeeker, error) {
 	b, err := base64.StdEncoding.DecodeString(s)
 	if err != nil {
 		return nil, err
@@ -655,6 +655,7 @@ func UpdateFile(ctx *context.APIContext) {
 	apiOpts := web.GetForm(ctx).(*api.UpdateFileOptions)
 	if ctx.Repo.Repository.IsEmpty {
 		ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
+		return
 	}
 
 	if apiOpts.BranchName == "" {
@@ -762,13 +763,13 @@ func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.Ch
 	}
 	message := ""
 	if len(createFiles) != 0 {
-		message += ctx.Tr("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
+		message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
 	}
 	if len(updateFiles) != 0 {
-		message += ctx.Tr("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
+		message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
 	}
 	if len(deleteFiles) != 0 {
-		message += ctx.Tr("repo.editor.delete", strings.Join(deleteFiles, ", "))
+		message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", "))
 	}
 	return strings.Trim(message, "\n")
 }
diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go
index 69433bf4cc..a1e3c9804b 100644
--- a/routers/api/v1/repo/fork.go
+++ b/routers/api/v1/repo/fork.go
@@ -14,11 +14,11 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
@@ -149,6 +149,8 @@ func CreateFork(ctx *context.APIContext) {
 	if err != nil {
 		if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
 			ctx.Error(http.StatusConflict, "ForkRepository", err)
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "ForkRepository", err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "ForkRepository", err)
 		}
diff --git a/routers/api/v1/repo/git_hook.go b/routers/api/v1/repo/git_hook.go
index 7e471e263b..26ae84d08d 100644
--- a/routers/api/v1/repo/git_hook.go
+++ b/routers/api/v1/repo/git_hook.go
@@ -6,10 +6,10 @@ package repo
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/git_ref.go b/routers/api/v1/repo/git_ref.go
index 34d2dcfcc8..0fa58425b8 100644
--- a/routers/api/v1/repo/git_ref.go
+++ b/routers/api/v1/repo/git_ref.go
@@ -7,10 +7,10 @@ import (
 	"net/http"
 	"net/url"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 )
 
 // GetGitAllRefs get ref or an list all the refs of a repository
diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go
index 8859e3ae23..ffd2313591 100644
--- a/routers/api/v1/repo/hook.go
+++ b/routers/api/v1/repo/hook.go
@@ -11,13 +11,13 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	"code.gitea.io/gitea/models/webhook"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	webhook_service "code.gitea.io/gitea/services/webhook"
 )
diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go
index 94a71e20ad..37cf61c1ed 100644
--- a/routers/api/v1/repo/hook_test.go
+++ b/routers/api/v1/repo/hook_test.go
@@ -9,7 +9,7 @@ import (
 
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/models/webhook"
-	"code.gitea.io/gitea/modules/contexttest"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 0f76a4b4ff..5e173abf88 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -5,6 +5,7 @@
 package repo
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -18,14 +19,14 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	issue_service "code.gitea.io/gitea/services/issue"
 	notify_service "code.gitea.io/gitea/services/notify"
@@ -122,14 +123,14 @@ func SearchIssues(ctx *context.APIContext) {
 		return
 	}
 
-	var isClosed util.OptionalBool
+	var isClosed optional.Option[bool]
 	switch ctx.FormString("state") {
 	case "closed":
-		isClosed = util.OptionalBoolTrue
+		isClosed = optional.Some(true)
 	case "all":
-		isClosed = util.OptionalBoolNone
+		isClosed = optional.None[bool]()
 	default:
-		isClosed = util.OptionalBoolFalse
+		isClosed = optional.Some(false)
 	}
 
 	var (
@@ -142,7 +143,7 @@ func SearchIssues(ctx *context.APIContext) {
 			Private:     false,
 			AllPublic:   true,
 			TopicOnly:   false,
-			Collaborate: util.OptionalBoolNone,
+			Collaborate: optional.None[bool](),
 			// This needs to be a column that is not nil in fixtures or
 			// MySQL will return different results when sorting by null in some cases
 			OrderBy: db.SearchOrderByAlphabetically,
@@ -165,7 +166,7 @@ func SearchIssues(ctx *context.APIContext) {
 			opts.OwnerID = owner.ID
 			opts.AllLimited = false
 			opts.AllPublic = false
-			opts.Collaborate = util.OptionalBoolFalse
+			opts.Collaborate = optional.Some(false)
 		}
 		if ctx.FormString("team") != "" {
 			if ctx.FormString("owner") == "" {
@@ -204,14 +205,14 @@ func SearchIssues(ctx *context.APIContext) {
 		keyword = ""
 	}
 
-	var isPull util.OptionalBool
+	var isPull optional.Option[bool]
 	switch ctx.FormString("type") {
 	case "pulls":
-		isPull = util.OptionalBoolTrue
+		isPull = optional.Some(true)
 	case "issues":
-		isPull = util.OptionalBoolFalse
+		isPull = optional.Some(false)
 	default:
-		isPull = util.OptionalBoolNone
+		isPull = optional.None[bool]()
 	}
 
 	var includedAnyLabels []int64
@@ -268,28 +269,28 @@ func SearchIssues(ctx *context.APIContext) {
 	}
 
 	if since != 0 {
-		searchOpt.UpdatedAfterUnix = &since
+		searchOpt.UpdatedAfterUnix = optional.Some(since)
 	}
 	if before != 0 {
-		searchOpt.UpdatedBeforeUnix = &before
+		searchOpt.UpdatedBeforeUnix = optional.Some(before)
 	}
 
 	if ctx.IsSigned {
 		ctxUserID := ctx.Doer.ID
 		if ctx.FormBool("created") {
-			searchOpt.PosterID = &ctxUserID
+			searchOpt.PosterID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("assigned") {
-			searchOpt.AssigneeID = &ctxUserID
+			searchOpt.AssigneeID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("mentioned") {
-			searchOpt.MentionID = &ctxUserID
+			searchOpt.MentionID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("review_requested") {
-			searchOpt.ReviewRequestedID = &ctxUserID
+			searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("reviewed") {
-			searchOpt.ReviewedID = &ctxUserID
+			searchOpt.ReviewedID = optional.Some(ctxUserID)
 		}
 	}
 
@@ -310,7 +311,7 @@ func SearchIssues(ctx *context.APIContext) {
 
 	ctx.SetLinkHeader(int(total), limit)
 	ctx.SetTotalCountHeader(total)
-	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
+	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
 }
 
 // ListIssues list the issues of a repository
@@ -367,7 +368,7 @@ func ListIssues(ctx *context.APIContext) {
 	//   required: false
 	// - name: created_by
 	//   in: query
-	//   description: Only show items which were created by the the given user
+	//   description: Only show items which were created by the given user
 	//   type: string
 	// - name: assigned_by
 	//   in: query
@@ -396,14 +397,14 @@ func ListIssues(ctx *context.APIContext) {
 		return
 	}
 
-	var isClosed util.OptionalBool
+	var isClosed optional.Option[bool]
 	switch ctx.FormString("state") {
 	case "closed":
-		isClosed = util.OptionalBoolTrue
+		isClosed = optional.Some(true)
 	case "all":
-		isClosed = util.OptionalBoolNone
+		isClosed = optional.None[bool]()
 	default:
-		isClosed = util.OptionalBoolFalse
+		isClosed = optional.Some(false)
 	}
 
 	keyword := ctx.FormTrim("q")
@@ -452,31 +453,29 @@ func ListIssues(ctx *context.APIContext) {
 
 	listOptions := utils.GetListOptions(ctx)
 
-	var isPull util.OptionalBool
+	isPull := optional.None[bool]()
 	switch ctx.FormString("type") {
 	case "pulls":
-		isPull = util.OptionalBoolTrue
+		isPull = optional.Some(true)
 	case "issues":
-		isPull = util.OptionalBoolFalse
-	default:
-		isPull = util.OptionalBoolNone
+		isPull = optional.Some(false)
 	}
 
-	if isPull != util.OptionalBoolNone && !ctx.Repo.CanReadIssuesOrPulls(isPull.IsTrue()) {
+	if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) {
 		ctx.NotFound()
 		return
 	}
 
-	if isPull == util.OptionalBoolNone {
+	if !isPull.Has() {
 		canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
 		canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
 		if !canReadIssues && !canReadPulls {
 			ctx.NotFound()
 			return
 		} else if !canReadIssues {
-			isPull = util.OptionalBoolTrue
+			isPull = optional.Some(true)
 		} else if !canReadPulls {
-			isPull = util.OptionalBoolFalse
+			isPull = optional.Some(false)
 		}
 	}
 
@@ -503,10 +502,10 @@ func ListIssues(ctx *context.APIContext) {
 		SortBy:    issue_indexer.SortByCreatedDesc,
 	}
 	if since != 0 {
-		searchOpt.UpdatedAfterUnix = &since
+		searchOpt.UpdatedAfterUnix = optional.Some(since)
 	}
 	if before != 0 {
-		searchOpt.UpdatedBeforeUnix = &before
+		searchOpt.UpdatedBeforeUnix = optional.Some(before)
 	}
 	if len(labelIDs) == 1 && labelIDs[0] == 0 {
 		searchOpt.NoLabelOnly = true
@@ -527,13 +526,13 @@ func ListIssues(ctx *context.APIContext) {
 	}
 
 	if createdByID > 0 {
-		searchOpt.PosterID = &createdByID
+		searchOpt.PosterID = optional.Some(createdByID)
 	}
 	if assignedByID > 0 {
-		searchOpt.AssigneeID = &assignedByID
+		searchOpt.AssigneeID = optional.Some(assignedByID)
 	}
 	if mentionedByID > 0 {
-		searchOpt.MentionID = &mentionedByID
+		searchOpt.MentionID = optional.Some(mentionedByID)
 	}
 
 	ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
@@ -549,7 +548,7 @@ func ListIssues(ctx *context.APIContext) {
 
 	ctx.SetLinkHeader(int(total), listOptions.PageSize)
 	ctx.SetTotalCountHeader(total)
-	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
+	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
 }
 
 func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
@@ -615,7 +614,7 @@ func GetIssue(ctx *context.APIContext) {
 		ctx.NotFound()
 		return
 	}
-	ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
+	ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue))
 }
 
 // CreateIssue create an issue of a repository
@@ -655,6 +654,7 @@ func CreateIssue(ctx *context.APIContext) {
 	//     "$ref": "#/responses/validationError"
 	//   "423":
 	//     "$ref": "#/responses/repoArchivedError"
+
 	form := web.GetForm(ctx).(*api.CreateIssueOption)
 	var deadlineUnix timeutil.TimeStamp
 	if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
@@ -709,12 +709,14 @@ func CreateIssue(ctx *context.APIContext) {
 		form.Labels = make([]int64, 0)
 	}
 
-	if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
+	if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil {
 		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
-			return
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "NewIssue", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "NewIssue", err)
 		}
-		ctx.Error(http.StatusInternalServerError, "NewIssue", err)
 		return
 	}
 
@@ -735,7 +737,7 @@ func CreateIssue(ctx *context.APIContext) {
 		ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
 		return
 	}
-	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, issue))
+	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, issue))
 }
 
 // EditIssue modify an issue of a repository
@@ -850,7 +852,11 @@ func EditIssue(ctx *context.APIContext) {
 
 		err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
 		if err != nil {
-			ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
+			if errors.Is(err, user_model.ErrBlockedUser) {
+				ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
+			} else {
+				ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
+			}
 			return
 		}
 	}
@@ -866,10 +872,11 @@ func EditIssue(ctx *context.APIContext) {
 	}
 	if form.State != nil {
 		if issue.IsPull {
-			if pr, err := issue.GetPullRequest(ctx); err != nil {
+			if err := issue.LoadPullRequest(ctx); err != nil {
 				ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
 				return
-			} else if pr.HasMerged {
+			}
+			if issue.PullRequest.HasMerged {
 				ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
 				return
 			}
@@ -904,7 +911,7 @@ func EditIssue(ctx *context.APIContext) {
 		ctx.InternalServerError(err)
 		return
 	}
-	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, issue))
+	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, issue))
 }
 
 func DeleteIssue(ctx *context.APIContext) {
diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go
index 11d19b21ff..7a5c6d554d 100644
--- a/routers/api/v1/repo/issue_attachment.go
+++ b/routers/api/v1/repo/issue_attachment.go
@@ -8,12 +8,12 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/attachment"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	issue_service "code.gitea.io/gitea/services/issue"
 )
@@ -107,7 +107,7 @@ func ListIssueAttachments(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue).Attachments)
+	ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue).Attachments)
 }
 
 // CreateIssueAttachment creates an attachment and saves the given file
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 4db2c68a79..070571ba62 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -14,11 +14,11 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	issue_service "code.gitea.io/gitea/services/issue"
 )
@@ -278,15 +278,15 @@ func ListRepoIssueComments(ctx *context.APIContext) {
 		return
 	}
 
-	var isPull util.OptionalBool
+	var isPull optional.Option[bool]
 	canReadIssue := ctx.Repo.CanRead(unit.TypeIssues)
 	canReadPull := ctx.Repo.CanRead(unit.TypePullRequests)
 	if canReadIssue && canReadPull {
-		isPull = util.OptionalBoolNone
+		isPull = optional.None[bool]()
 	} else if canReadIssue {
-		isPull = util.OptionalBoolFalse
+		isPull = optional.Some(false)
 	} else if canReadPull {
-		isPull = util.OptionalBoolTrue
+		isPull = optional.Some(true)
 	} else {
 		ctx.NotFound()
 		return
@@ -323,10 +323,6 @@ func ListRepoIssueComments(ctx *context.APIContext) {
 		ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
 		return
 	}
-	if err := comments.LoadPosters(ctx); err != nil {
-		ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
-		return
-	}
 	if err := comments.LoadAttachments(ctx); err != nil {
 		ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
 		return
@@ -382,6 +378,7 @@ func CreateIssueComment(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 	//   "423":
 	//     "$ref": "#/responses/repoArchivedError"
+
 	form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
 	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
@@ -395,13 +392,17 @@ func CreateIssueComment(ctx *context.APIContext) {
 	}
 
 	if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
-		ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
+		ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked")))
 		return
 	}
 
 	comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
+		}
 		return
 	}
 
@@ -522,6 +523,7 @@ func EditIssueComment(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 	//   "423":
 	//     "$ref": "#/responses/repoArchivedError"
+
 	form := web.GetForm(ctx).(*api.EditIssueCommentOption)
 	editIssueComment(ctx, *form)
 }
@@ -610,7 +612,11 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
 	oldContent := comment.Content
 	comment.Content = form.Body
 	if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
-		ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "UpdateComment", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
+		}
 		return
 	}
 
diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go
index 21e2f4dabd..4096cbf07b 100644
--- a/routers/api/v1/repo/issue_comment_attachment.go
+++ b/routers/api/v1/repo/issue_comment_attachment.go
@@ -4,16 +4,18 @@
 package repo
 
 import (
+	"errors"
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/attachment"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	issue_service "code.gitea.io/gitea/services/issue"
 )
@@ -154,6 +156,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
 	//     "$ref": "#/responses/Attachment"
 	//   "400":
 	//     "$ref": "#/responses/error"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/error"
 	//   "423":
@@ -199,7 +203,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
 	}
 
 	if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
-		ctx.ServerError("UpdateComment", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "UpdateComment", err)
+		} else {
+			ctx.ServerError("UpdateComment", err)
+		}
 		return
 	}
 
diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go
index 62d1057cdf..c40e92c01b 100644
--- a/routers/api/v1/repo/issue_dependency.go
+++ b/routers/api/v1/repo/issue_dependency.go
@@ -11,10 +11,10 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -153,7 +153,7 @@ func GetIssueDependencies(ctx *context.APIContext) {
 		blockerIssues = append(blockerIssues, &blocker.Issue)
 	}
 
-	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, blockerIssues))
+	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, blockerIssues))
 }
 
 // CreateIssueDependency create a new issue dependencies
@@ -214,7 +214,7 @@ func CreateIssueDependency(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target))
+	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
 }
 
 // RemoveIssueDependency remove an issue dependency
@@ -275,7 +275,7 @@ func RemoveIssueDependency(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target))
+	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
 }
 
 // GetIssueBlocks list issues that are blocked by this issue
@@ -381,7 +381,7 @@ func GetIssueBlocks(ctx *context.APIContext) {
 		issues = append(issues, &depMeta.Issue)
 	}
 
-	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
+	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
 }
 
 // CreateIssueBlocking block the issue given in the body by the issue in path
@@ -438,7 +438,7 @@ func CreateIssueBlocking(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency))
+	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
 }
 
 // RemoveIssueBlocking unblock the issue given in the body by the issue in path
@@ -495,7 +495,7 @@ func RemoveIssueBlocking(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency))
+	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
 }
 
 func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go
index c2f530956e..7d9f85d2aa 100644
--- a/routers/api/v1/repo/issue_label.go
+++ b/routers/api/v1/repo/issue_label.go
@@ -8,9 +8,9 @@ import (
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	issue_service "code.gitea.io/gitea/services/issue"
 )
diff --git a/routers/api/v1/repo/issue_pin.go b/routers/api/v1/repo/issue_pin.go
index 61f88de34e..af3e06332a 100644
--- a/routers/api/v1/repo/issue_pin.go
+++ b/routers/api/v1/repo/issue_pin.go
@@ -7,8 +7,8 @@ import (
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -207,7 +207,7 @@ func ListPinnedIssues(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
+	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
 }
 
 // ListPinnedPullRequests returns a list of all pinned PRs
@@ -240,18 +240,12 @@ func ListPinnedPullRequests(ctx *context.APIContext) {
 	}
 
 	apiPrs := make([]*api.PullRequest, len(issues))
+	if err := issues.LoadPullRequests(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadPullRequests", err)
+		return
+	}
 	for i, currentIssue := range issues {
-		pr, err := currentIssue.GetPullRequest(ctx)
-		if err != nil {
-			ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
-			return
-		}
-
-		if err = pr.LoadIssue(ctx); err != nil {
-			ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
-			return
-		}
-
+		pr := currentIssue.PullRequest
 		if err = pr.LoadAttributes(ctx); err != nil {
 			ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
 			return
diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go
index c886bd71b7..3ff3d19f13 100644
--- a/routers/api/v1/repo/issue_reaction.go
+++ b/routers/api/v1/repo/issue_reaction.go
@@ -8,11 +8,13 @@ import (
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
+	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
+	issue_service "code.gitea.io/gitea/services/issue"
 )
 
 // GetIssueCommentReactions list reactions of a comment from an issue
@@ -218,9 +220,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
 
 	if isCreateType {
 		// PostIssueCommentReaction part
-		reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
+		reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction)
 		if err != nil {
-			if issues_model.IsErrForbiddenIssueReaction(err) {
+			if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
 				ctx.Error(http.StatusForbidden, err.Error(), err)
 			} else if issues_model.IsErrReactionAlreadyExist(err) {
 				ctx.JSON(http.StatusOK, api.Reaction{
@@ -434,9 +436,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
 
 	if isCreateType {
 		// PostIssueReaction part
-		reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction)
+		reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
 		if err != nil {
-			if issues_model.IsErrForbiddenIssueReaction(err) {
+			if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
 				ctx.Error(http.StatusForbidden, err.Error(), err)
 			} else if issues_model.IsErrReactionAlreadyExist(err) {
 				ctx.JSON(http.StatusOK, api.Reaction{
@@ -445,7 +447,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
 					Created:  reaction.CreatedUnix.AsTime(),
 				})
 			} else {
-				ctx.Error(http.StatusInternalServerError, "CreateCommentReaction", err)
+				ctx.Error(http.StatusInternalServerError, "CreateIssueReaction", err)
 			}
 			return
 		}
diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go
index 52bf8b5c7b..d9054e8f77 100644
--- a/routers/api/v1/repo/issue_stopwatch.go
+++ b/routers/api/v1/repo/issue_stopwatch.go
@@ -8,8 +8,8 @@ import (
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go
index ece880c03e..a535172462 100644
--- a/routers/api/v1/repo/issue_subscription.go
+++ b/routers/api/v1/repo/issue_subscription.go
@@ -9,9 +9,9 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go
index cf03e72aa0..f83855efac 100644
--- a/routers/api/v1/repo/issue_tracked_time.go
+++ b/routers/api/v1/repo/issue_tracked_time.go
@@ -12,10 +12,10 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -138,7 +138,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
 	}
 
 	ctx.SetTotalCountHeader(count)
-	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
+	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
 }
 
 // AddTime add time manual to the given issue
@@ -225,7 +225,7 @@ func AddTime(ctx *context.APIContext) {
 		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
 		return
 	}
-	ctx.JSON(http.StatusOK, convert.ToTrackedTime(ctx, trackedTime))
+	ctx.JSON(http.StatusOK, convert.ToTrackedTime(ctx, user, trackedTime))
 }
 
 // ResetIssueTime reset time manual to the given issue
@@ -455,7 +455,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
 		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
 		return
 	}
-	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
+	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
 }
 
 // ListTrackedTimesByRepository lists all tracked times of the repository
@@ -567,7 +567,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
 	}
 
 	ctx.SetTotalCountHeader(count)
-	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
+	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
 }
 
 // ListMyTrackedTimes lists all tracked times of the current user
@@ -629,5 +629,5 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
 	}
 
 	ctx.SetTotalCountHeader(count)
-	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
+	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
 }
diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go
index af48c40885..88444a2625 100644
--- a/routers/api/v1/repo/key.go
+++ b/routers/api/v1/repo/key.go
@@ -15,12 +15,12 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go
index 420d3ab5b4..b6eb51fd20 100644
--- a/routers/api/v1/repo/label.go
+++ b/routers/api/v1/repo/label.go
@@ -9,11 +9,11 @@ import (
 	"strconv"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/label"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/language.go b/routers/api/v1/repo/language.go
index 12f1761ad0..f1d5bbe45f 100644
--- a/routers/api/v1/repo/language.go
+++ b/routers/api/v1/repo/language.go
@@ -9,8 +9,8 @@ import (
 	"strconv"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 type languageResponse []*repo_model.LanguageStat
diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go
index 839fbfe8a1..2caaa130e8 100644
--- a/routers/api/v1/repo/migrate.go
+++ b/routers/api/v1/repo/migrate.go
@@ -17,7 +17,6 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
@@ -26,6 +25,7 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/migrations"
diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go
index 9c2ed16d93..b9534016e4 100644
--- a/routers/api/v1/repo/milestone.go
+++ b/routers/api/v1/repo/milestone.go
@@ -11,12 +11,12 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -61,10 +61,10 @@ func ListMilestones(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 
 	state := api.StateType(ctx.FormString("state"))
-	var isClosed util.OptionalBool
+	var isClosed optional.Option[bool]
 	switch state {
 	case api.StateClosed, api.StateOpen:
-		isClosed = util.OptionalBoolOf(state == api.StateClosed)
+		isClosed = optional.Some(state == api.StateClosed)
 	}
 
 	milestones, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go
index 26e0be301c..864644e1ef 100644
--- a/routers/api/v1/repo/mirror.go
+++ b/routers/api/v1/repo/mirror.go
@@ -13,12 +13,12 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/migrations"
diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go
index e7e00dae41..a4a1d4eab7 100644
--- a/routers/api/v1/repo/notes.go
+++ b/routers/api/v1/repo/notes.go
@@ -7,9 +7,9 @@ import (
 	"fmt"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go
index 9b5635d245..0e0601b7d9 100644
--- a/routers/api/v1/repo/patch.go
+++ b/routers/api/v1/repo/patch.go
@@ -10,10 +10,10 @@ import (
 	"code.gitea.io/gitea/models"
 	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/repository/files"
 )
 
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index eaf406e64d..e43366ff14 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -21,7 +21,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
@@ -32,6 +32,7 @@ import (
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	"code.gitea.io/gitea/services/automerge"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/gitdiff"
@@ -96,13 +97,17 @@ func ListPullRequests(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
+	labelIDs, err := base.StringsToInt64s(ctx.FormStrings("labels"))
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "PullRequests", err)
+		return
+	}
 	listOptions := utils.GetListOptions(ctx)
-
 	prs, maxResults, err := issues_model.PullRequests(ctx, ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{
 		ListOptions: listOptions,
 		State:       ctx.FormTrim("state"),
 		SortType:    ctx.FormTrim("sort"),
-		Labels:      ctx.FormStrings("labels"),
+		Labels:      labelIDs,
 		MilestoneID: ctx.FormInt64("milestone"),
 	})
 	if err != nil {
@@ -187,6 +192,91 @@ func GetPullRequest(ctx *context.APIContext) {
 	ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
 }
 
+// GetPullRequest returns a single PR based on index
+func GetPullRequestByBaseHead(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/pulls/{base}/{head} repository repoGetPullRequestByBaseHead
+	// ---
+	// summary: Get a pull request by base and head
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// - name: base
+	//   in: path
+	//   description: base of the pull request to get
+	//   type: string
+	//   required: true
+	// - name: head
+	//   in: path
+	//   description: head of the pull request to get
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/PullRequest"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	var headRepoID int64
+	var headBranch string
+	head := ctx.Params("*")
+	if strings.Contains(head, ":") {
+		split := strings.SplitN(head, ":", 2)
+		headBranch = split[1]
+		var owner, name string
+		if strings.Contains(split[0], "/") {
+			split = strings.Split(split[0], "/")
+			owner = split[0]
+			name = split[1]
+		} else {
+			owner = split[0]
+			name = ctx.Repo.Repository.Name
+		}
+		repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, name)
+		if err != nil {
+			if repo_model.IsErrRepoNotExist(err) {
+				ctx.NotFound()
+			} else {
+				ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerName", err)
+			}
+			return
+		}
+		headRepoID = repo.ID
+	} else {
+		headRepoID = ctx.Repo.Repository.ID
+		headBranch = head
+	}
+
+	pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.Params(":base"), headBranch)
+	if err != nil {
+		if issues_model.IsErrPullRequestNotExist(err) {
+			ctx.NotFound()
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetPullRequestByBaseHeadInfo", err)
+		}
+		return
+	}
+
+	if err = pr.LoadBaseRepo(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
+		return
+	}
+	if err = pr.LoadHeadRepo(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
+		return
+	}
+	ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
+}
+
 // DownloadPullDiffOrPatch render a pull's raw diff or patch
 func DownloadPullDiffOrPatch(ctx *context.APIContext) {
 	// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}.{diffType} repository repoDownloadPullDiffOrPatch
@@ -277,6 +367,8 @@ func CreatePullRequest(ctx *context.APIContext) {
 	// responses:
 	//   "201":
 	//     "$ref": "#/responses/PullRequest"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 	//   "409":
@@ -425,9 +517,11 @@ func CreatePullRequest(ctx *context.APIContext) {
 	if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
 		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
-			return
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "BlockedUser", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
 		}
-		ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
 		return
 	}
 
@@ -545,6 +639,8 @@ func EditPullRequest(ctx *context.APIContext) {
 		if err != nil {
 			if user_model.IsErrUserNotExist(err) {
 				ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
+			} else if errors.Is(err, user_model.ErrBlockedUser) {
+				ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
 			} else {
 				ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
 			}
@@ -977,6 +1073,8 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
 			return nil, nil, nil, nil, "", ""
 		}
 		headBranch = headInfos[1]
+		// The head repository can also point to the same repo
+		isSameRepo = ctx.Repo.Owner.ID == headUser.ID
 
 	} else {
 		ctx.NotFound()
diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go
index 07d8f4877b..17bb2085b6 100644
--- a/routers/api/v1/repo/pull_review.go
+++ b/routers/api/v1/repo/pull_review.go
@@ -12,11 +12,11 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/gitrepo"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	issue_service "code.gitea.io/gitea/services/issue"
 	pull_service "code.gitea.io/gitea/services/pull"
@@ -362,6 +362,7 @@ func CreatePullReview(ctx *context.APIContext) {
 			true, // pending review
 			0,    // no reply
 			opts.CommitID,
+			nil,
 		); err != nil {
 			ctx.Error(http.StatusInternalServerError, "CreateCodeComment", err)
 			return
@@ -544,7 +545,7 @@ func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues
 		return nil, nil, true
 	}
 
-	// validate the the review is for the given PR
+	// validate the review is for the given PR
 	if review.IssueID != pr.IssueID {
 		ctx.NotFound("ReviewNotInPR")
 		return nil, nil, true
@@ -639,6 +640,8 @@ func DeleteReviewRequests(ctx *context.APIContext) {
 	//     "$ref": "#/responses/empty"
 	//   "422":
 	//     "$ref": "#/responses/validationError"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 	opts := web.GetForm(ctx).(*api.PullReviewRequestOptions)
@@ -707,6 +710,10 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
 	for _, reviewer := range reviewers {
 		comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, reviewer, isAdd)
 		if err != nil {
+			if issues_model.IsErrReviewRequestOnClosedPR(err) {
+				ctx.Error(http.StatusForbidden, "", err)
+				return
+			}
 			ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
 			return
 		}
@@ -873,7 +880,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors
 		ctx.Error(http.StatusForbidden, "", "Must be repo admin")
 		return
 	}
-	review, pr, isWrong := prepareSingleReview(ctx)
+	review, _, isWrong := prepareSingleReview(ctx)
 	if isWrong {
 		return
 	}
@@ -883,13 +890,12 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors
 		return
 	}
 
-	if pr.Issue.IsClosed {
-		ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because this pr is closed")
-		return
-	}
-
 	_, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors)
 	if err != nil {
+		if pull_service.IsErrDismissRequestOnClosedPR(err) {
+			ctx.Error(http.StatusForbidden, "", err)
+			return
+		}
 		ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err)
 		return
 	}
diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go
index a41c5ba7d8..f0f3c0bbc7 100644
--- a/routers/api/v1/repo/release.go
+++ b/routers/api/v1/repo/release.go
@@ -4,6 +4,7 @@
 package repo
 
 import (
+	"fmt"
 	"net/http"
 
 	"code.gitea.io/gitea/models"
@@ -11,10 +12,10 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	release_service "code.gitea.io/gitea/services/release"
 )
@@ -215,6 +216,10 @@ func CreateRelease(ctx *context.APIContext) {
 	//   "409":
 	//     "$ref": "#/responses/error"
 	form := web.GetForm(ctx).(*api.CreateReleaseOption)
+	if ctx.Repo.Repository.IsEmpty {
+		ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
+		return
+	}
 	rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName)
 	if err != nil {
 		if !repo_model.IsErrReleaseNotExist(err) {
diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go
index c36bf12e6d..59fd83e3a2 100644
--- a/routers/api/v1/repo/release_attachment.go
+++ b/routers/api/v1/repo/release_attachment.go
@@ -4,16 +4,18 @@
 package repo
 
 import (
+	"io"
 	"net/http"
+	"strings"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/attachment"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -154,6 +156,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
 	// - application/json
 	// consumes:
 	// - multipart/form-data
+	// - application/octet-stream
 	// parameters:
 	// - name: owner
 	//   in: path
@@ -180,7 +183,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
 	//   in: formData
 	//   description: attachment to upload
 	//   type: file
-	//   required: true
+	//   required: false
 	// responses:
 	//   "201":
 	//     "$ref": "#/responses/Attachment"
@@ -202,20 +205,36 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
 	}
 
 	// Get uploaded file from request
-	file, header, err := ctx.Req.FormFile("attachment")
-	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "GetFile", err)
-		return
-	}
-	defer file.Close()
+	var content io.ReadCloser
+	var filename string
+	var size int64 = -1
 
-	filename := header.Filename
-	if query := ctx.FormString("name"); query != "" {
-		filename = query
+	if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") {
+		file, header, err := ctx.Req.FormFile("attachment")
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, "GetFile", err)
+			return
+		}
+		defer file.Close()
+
+		content = file
+		size = header.Size
+		filename = header.Filename
+		if name := ctx.FormString("name"); name != "" {
+			filename = name
+		}
+	} else {
+		content = ctx.Req.Body
+		filename = ctx.FormString("name")
+	}
+
+	if filename == "" {
+		ctx.Error(http.StatusBadRequest, "CreateReleaseAttachment", "Could not determine name of attachment.")
+		return
 	}
 
 	// Create a new attachment and save the file
-	attach, err := attachment.UploadAttachment(ctx, file, setting.Repository.Release.AllowedTypes, header.Size, &repo_model.Attachment{
+	attach, err := attachment.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{
 		Name:       filename,
 		UploaderID: ctx.Doer.ID,
 		RepoID:     ctx.Repo.Repository.ID,
diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go
index 9f2098df06..fec91164a2 100644
--- a/routers/api/v1/repo/release_tags.go
+++ b/routers/api/v1/repo/release_tags.go
@@ -8,7 +8,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	releaseservice "code.gitea.io/gitea/services/release"
 )
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 2efdccb569..822e368fa8 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -8,9 +8,11 @@ import (
 	"fmt"
 	"net/http"
 	"slices"
+	"strconv"
 	"strings"
 	"time"
 
+	actions_model "code.gitea.io/gitea/models/actions"
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
@@ -19,18 +21,19 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/label"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	actions_service "code.gitea.io/gitea/services/actions"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/issue"
 	repo_service "code.gitea.io/gitea/services/repository"
@@ -134,33 +137,33 @@ func Search(ctx *context.APIContext) {
 		PriorityOwnerID:    ctx.FormInt64("priority_owner_id"),
 		TeamID:             ctx.FormInt64("team_id"),
 		TopicOnly:          ctx.FormBool("topic"),
-		Collaborate:        util.OptionalBoolNone,
+		Collaborate:        optional.None[bool](),
 		Private:            ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
-		Template:           util.OptionalBoolNone,
+		Template:           optional.None[bool](),
 		StarredByID:        ctx.FormInt64("starredBy"),
 		IncludeDescription: ctx.FormBool("includeDesc"),
 	}
 
 	if ctx.FormString("template") != "" {
-		opts.Template = util.OptionalBoolOf(ctx.FormBool("template"))
+		opts.Template = optional.Some(ctx.FormBool("template"))
 	}
 
 	if ctx.FormBool("exclusive") {
-		opts.Collaborate = util.OptionalBoolFalse
+		opts.Collaborate = optional.Some(false)
 	}
 
 	mode := ctx.FormString("mode")
 	switch mode {
 	case "source":
-		opts.Fork = util.OptionalBoolFalse
-		opts.Mirror = util.OptionalBoolFalse
+		opts.Fork = optional.Some(false)
+		opts.Mirror = optional.Some(false)
 	case "fork":
-		opts.Fork = util.OptionalBoolTrue
+		opts.Fork = optional.Some(true)
 	case "mirror":
-		opts.Mirror = util.OptionalBoolTrue
+		opts.Mirror = optional.Some(true)
 	case "collaborative":
-		opts.Mirror = util.OptionalBoolFalse
-		opts.Collaborate = util.OptionalBoolTrue
+		opts.Mirror = optional.Some(false)
+		opts.Collaborate = optional.Some(true)
 	case "":
 	default:
 		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode))
@@ -168,11 +171,11 @@ func Search(ctx *context.APIContext) {
 	}
 
 	if ctx.FormString("archived") != "" {
-		opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived"))
+		opts.Archived = optional.Some(ctx.FormBool("archived"))
 	}
 
 	if ctx.FormString("is_private") != "" {
-		opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private"))
+		opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
 	}
 
 	sortMode := ctx.FormString("sort")
@@ -357,7 +360,7 @@ func Generate(ctx *context.APIContext) {
 		return
 	}
 
-	opts := repo_module.GenerateRepoOptions{
+	opts := repo_service.GenerateRepoOptions{
 		Name:            form.Name,
 		DefaultBranch:   form.DefaultBranch,
 		Description:     form.Description,
@@ -719,7 +722,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
 
 	if ctx.Repo.GitRepo == nil && !repo.IsEmpty {
 		var err error
-		ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
+		ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo)
 		if err != nil {
 			ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err)
 			return err
@@ -730,7 +733,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
 	// Default branch only updated if changed and exist or the repository is empty
 	if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) {
 		if !repo.IsEmpty {
-			if err := ctx.Repo.GitRepo.SetDefaultBranch(*opts.DefaultBranch); err != nil {
+			if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, *opts.DefaultBranch); err != nil {
 				if !git.IsErrUnsupportedVersion(err) {
 					ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err)
 					return err
@@ -884,6 +887,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
 					AllowRebase:                   true,
 					AllowRebaseMerge:              true,
 					AllowSquash:                   true,
+					AllowFastForwardOnly:          true,
 					AllowManualMerge:              true,
 					AutodetectManualMerge:         false,
 					AllowRebaseUpdate:             true,
@@ -910,6 +914,9 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
 			if opts.AllowSquash != nil {
 				config.AllowSquash = *opts.AllowSquash
 			}
+			if opts.AllowFastForwardOnly != nil {
+				config.AllowFastForwardOnly = *opts.AllowFastForwardOnly
+			}
 			if opts.AllowManualMerge != nil {
 				config.AllowManualMerge = *opts.AllowManualMerge
 			}
@@ -939,13 +946,33 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
 		}
 	}
 
-	if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() {
-		if *opts.HasProjects {
+	currHasProjects := repo.UnitEnabled(ctx, unit_model.TypeProjects)
+	newHasProjects := currHasProjects
+	if opts.HasProjects != nil {
+		newHasProjects = *opts.HasProjects
+	}
+	if currHasProjects || newHasProjects {
+		if newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
+			unit, err := repo.GetUnit(ctx, unit_model.TypeProjects)
+			var config *repo_model.ProjectsConfig
+			if err != nil {
+				config = &repo_model.ProjectsConfig{
+					ProjectsMode: repo_model.ProjectsModeAll,
+				}
+			} else {
+				config = unit.ProjectsConfig()
+			}
+
+			if opts.ProjectsMode != nil {
+				config.ProjectsMode = repo_model.ProjectsMode(*opts.ProjectsMode)
+			}
+
 			units = append(units, repo_model.RepoUnit{
 				RepoID: repo.ID,
 				Type:   unit_model.TypeProjects,
+				Config: config,
 			})
-		} else {
+		} else if !newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
 			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
 		}
 	}
@@ -1010,6 +1037,9 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
 				ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
 				return err
 			}
+			if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
+				log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+			}
 			log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
 		} else {
 			if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil {
@@ -1017,6 +1047,11 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
 				ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
 				return err
 			}
+			if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
+				if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
+					log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+				}
+			}
 			log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
 		}
 	}
@@ -1161,12 +1196,11 @@ func GetIssueTemplates(ctx *context.APIContext) {
 	//     "$ref": "#/responses/IssueTemplates"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
-	ret, err := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
-	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "GetTemplatesFromDefaultBranch", err)
-		return
+	ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+	if cnt := len(ret.TemplateErrors); cnt != 0 {
+		ctx.Resp.Header().Add("X-Gitea-Warning", "error occurs when parsing issue template: count="+strconv.Itoa(cnt))
 	}
-	ctx.JSON(http.StatusOK, ret)
+	ctx.JSON(http.StatusOK, ret.IssueTemplates)
 }
 
 // GetIssueConfig returns the issue config for a repo
diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go
index 29e2d1f21d..8d6ca9e3b5 100644
--- a/routers/api/v1/repo/repo_test.go
+++ b/routers/api/v1/repo/repo_test.go
@@ -9,9 +9,9 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -35,6 +35,7 @@ func TestRepoEdit(t *testing.T) {
 	allowRebase := false
 	allowRebaseMerge := false
 	allowSquashMerge := false
+	allowFastForwardOnlyMerge := false
 	archived := true
 	opts := api.EditRepoOption{
 		Name:                      &ctx.Repo.Repository.Name,
@@ -50,6 +51,7 @@ func TestRepoEdit(t *testing.T) {
 		AllowRebase:               &allowRebase,
 		AllowRebaseMerge:          &allowRebaseMerge,
 		AllowSquash:               &allowSquashMerge,
+		AllowFastForwardOnly:      &allowFastForwardOnlyMerge,
 		Archived:                  &archived,
 	}
 
diff --git a/routers/api/v1/repo/runners.go b/routers/api/v1/repo/runners.go
index 0a2bbf8117..fe133b311d 100644
--- a/routers/api/v1/repo/runners.go
+++ b/routers/api/v1/repo/runners.go
@@ -4,8 +4,8 @@
 package repo
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/shared"
+	"code.gitea.io/gitea/services/context"
 )
 
 // GetRegistrationToken returns the token to register repo runners
diff --git a/routers/api/v1/repo/star.go b/routers/api/v1/repo/star.go
index 05227e33a0..99676de119 100644
--- a/routers/api/v1/repo/star.go
+++ b/routers/api/v1/repo/star.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go
index b4edf0608c..9e36ea0aed 100644
--- a/routers/api/v1/repo/status.go
+++ b/routers/api/v1/repo/status.go
@@ -9,12 +9,12 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
-	files_service "code.gitea.io/gitea/services/repository/files"
+	commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
 )
 
 // NewCommitStatus creates a new CommitStatus
@@ -64,7 +64,7 @@ func NewCommitStatus(ctx *context.APIContext) {
 		Description: form.Description,
 		Context:     form.Context,
 	}
-	if err := files_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil {
+	if err := commitstatus_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil {
 		ctx.Error(http.StatusInternalServerError, "CreateCommitStatus", err)
 		return
 	}
diff --git a/routers/api/v1/repo/subscriber.go b/routers/api/v1/repo/subscriber.go
index 05509fc443..8584182857 100644
--- a/routers/api/v1/repo/subscriber.go
+++ b/routers/api/v1/repo/subscriber.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go
index 2f19f95e66..a6908f3615 100644
--- a/routers/api/v1/repo/tag.go
+++ b/routers/api/v1/repo/tag.go
@@ -10,10 +10,10 @@ import (
 
 	"code.gitea.io/gitea/models"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	releaseservice "code.gitea.io/gitea/services/release"
 )
diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go
index 1bacc71211..0ecf3a39d8 100644
--- a/routers/api/v1/repo/teams.go
+++ b/routers/api/v1/repo/teams.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/models/organization"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	org_service "code.gitea.io/gitea/services/org"
 	repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/api/v1/repo/topic.go b/routers/api/v1/repo/topic.go
index d662b9b583..9852caa989 100644
--- a/routers/api/v1/repo/topic.go
+++ b/routers/api/v1/repo/topic.go
@@ -7,12 +7,13 @@ import (
 	"net/http"
 	"strings"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -53,7 +54,7 @@ func ListTopics(ctx *context.APIContext) {
 		RepoID:      ctx.Repo.Repository.ID,
 	}
 
-	topics, total, err := repo_model.FindTopics(ctx, opts)
+	topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
@@ -172,7 +173,7 @@ func AddTopic(ctx *context.APIContext) {
 	}
 
 	// Prevent adding more topics than allowed to repo
-	count, err := repo_model.CountTopics(ctx, &repo_model.FindTopicOptions{
+	count, err := db.Count[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
 		RepoID: ctx.Repo.Repository.ID,
 	})
 	if err != nil {
@@ -287,7 +288,7 @@ func TopicSearch(ctx *context.APIContext) {
 		ListOptions: utils.GetListOptions(ctx),
 	}
 
-	topics, total, err := repo_model.FindTopics(ctx, opts)
+	topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go
index c0a40ce062..776b336761 100644
--- a/routers/api/v1/repo/transfer.go
+++ b/routers/api/v1/repo/transfer.go
@@ -4,6 +4,7 @@
 package repo
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 
@@ -13,10 +14,10 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
@@ -117,7 +118,11 @@ func Transfer(ctx *context.APIContext) {
 			return
 		}
 
-		ctx.InternalServerError(err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "BlockedUser", err)
+		} else {
+			ctx.InternalServerError(err)
+		}
 		return
 	}
 
diff --git a/routers/api/v1/repo/tree.go b/routers/api/v1/repo/tree.go
index f63100b6ea..353a996d5b 100644
--- a/routers/api/v1/repo/tree.go
+++ b/routers/api/v1/repo/tree.go
@@ -6,7 +6,7 @@ package repo
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	files_service "code.gitea.io/gitea/services/repository/files"
 )
 
diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go
index 4f27500496..f18ea087c4 100644
--- a/routers/api/v1/repo/wiki.go
+++ b/routers/api/v1/repo/wiki.go
@@ -10,13 +10,13 @@ import (
 	"net/url"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	notify_service "code.gitea.io/gitea/services/notify"
 	wiki_service "code.gitea.io/gitea/services/wiki"
diff --git a/routers/api/v1/settings/settings.go b/routers/api/v1/settings/settings.go
index 02bda1309d..0ee81b96d5 100644
--- a/routers/api/v1/settings/settings.go
+++ b/routers/api/v1/settings/settings.go
@@ -6,9 +6,9 @@ package settings
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 )
 
 // GetGeneralUISettings returns instance's global settings for ui
diff --git a/routers/api/v1/shared/block.go b/routers/api/v1/shared/block.go
new file mode 100644
index 0000000000..a1e65625ed
--- /dev/null
+++ b/routers/api/v1/shared/block.go
@@ -0,0 +1,98 @@
+// Copyright 2024 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package shared
+
+import (
+	"errors"
+	"net/http"
+
+	user_model "code.gitea.io/gitea/models/user"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/convert"
+	user_service "code.gitea.io/gitea/services/user"
+)
+
+func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
+	blocks, total, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
+		ListOptions: utils.GetListOptions(ctx),
+		BlockerID:   blocker.ID,
+	})
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "FindBlockings", err)
+		return
+	}
+
+	if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
+		return
+	}
+
+	users := make([]*api.User, 0, len(blocks))
+	for _, b := range blocks {
+		users = append(users, convert.ToUser(ctx, b.Blockee, blocker))
+	}
+
+	ctx.SetTotalCountHeader(total)
+	ctx.JSON(http.StatusOK, &users)
+}
+
+func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) {
+	blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
+	if err != nil {
+		ctx.NotFound("GetUserByName", err)
+		return
+	}
+
+	status := http.StatusNotFound
+	blocking, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetBlocking", err)
+		return
+	}
+	if blocking != nil {
+		status = http.StatusNoContent
+	}
+
+	ctx.Status(status)
+}
+
+func BlockUser(ctx *context.APIContext, blocker *user_model.User) {
+	blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
+	if err != nil {
+		ctx.NotFound("GetUserByName", err)
+		return
+	}
+
+	if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil {
+		if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
+			ctx.Error(http.StatusBadRequest, "BlockUser", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "BlockUser", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) {
+	blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
+	if err != nil {
+		ctx.NotFound("GetUserByName", err)
+		return
+	}
+
+	if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil {
+		if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
+			ctx.Error(http.StatusBadRequest, "UnblockUser", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "UnblockUser", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go
index a342bd4b63..c850ad7866 100644
--- a/routers/api/v1/shared/runners.go
+++ b/routers/api/v1/shared/runners.go
@@ -8,8 +8,8 @@ import (
 	"net/http"
 
 	actions_model "code.gitea.io/gitea/models/actions"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 // RegistrationToken is response related to registeration token
diff --git a/routers/api/v1/swagger/action.go b/routers/api/v1/swagger/action.go
index 3771780718..665f4d0b85 100644
--- a/routers/api/v1/swagger/action.go
+++ b/routers/api/v1/swagger/action.go
@@ -18,3 +18,17 @@ type swaggerResponseSecret struct {
 	// in:body
 	Body api.Secret `json:"body"`
 }
+
+// ActionVariable
+// swagger:response ActionVariable
+type swaggerResponseActionVariable struct {
+	// in:body
+	Body api.ActionVariable `json:"body"`
+}
+
+// VariableList
+// swagger:response VariableList
+type swaggerResponseVariableList struct {
+	// in:body
+	Body []api.ActionVariable `json:"body"`
+}
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 6f7859df62..cd551cbdfa 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -190,4 +190,13 @@ type swaggerParameterBodies struct {
 
 	// in:body
 	CreateOrUpdateSecretOption api.CreateOrUpdateSecretOption
+
+	// in:body
+	UserBadgeOption api.UserBadgeOption
+
+	// in:body
+	CreateVariableOption api.CreateVariableOption
+
+	// in:body
+	UpdateVariableOption api.UpdateVariableOption
 }
diff --git a/routers/api/v1/swagger/user.go b/routers/api/v1/swagger/user.go
index fb6d185ee7..e2ad511d2b 100644
--- a/routers/api/v1/swagger/user.go
+++ b/routers/api/v1/swagger/user.go
@@ -48,3 +48,10 @@ type swaggerResponseUserSettings struct {
 	// in:body
 	Body []api.UserSettings `json:"body"`
 }
+
+// BadgeList
+// swagger:response BadgeList
+type swaggerResponseBadgeList struct {
+	// in:body
+	Body []api.Badge `json:"body"`
+}
diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go
index cbe332a779..bf78c2c864 100644
--- a/routers/api/v1/user/action.go
+++ b/routers/api/v1/user/action.go
@@ -7,10 +7,14 @@ import (
 	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/routers/api/v1/utils"
+	actions_service "code.gitea.io/gitea/services/actions"
+	"code.gitea.io/gitea/services/context"
 	secret_service "code.gitea.io/gitea/services/secrets"
 )
 
@@ -101,3 +105,249 @@ func DeleteSecret(ctx *context.APIContext) {
 
 	ctx.Status(http.StatusNoContent)
 }
+
+// CreateVariable create a user-level variable
+func CreateVariable(ctx *context.APIContext) {
+	// swagger:operation POST /user/actions/variables/{variablename} user createUserVariable
+	// ---
+	// summary: Create a user-level variable
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/CreateVariableOption"
+	// responses:
+	//   "201":
+	//     description: response when creating a variable
+	//   "204":
+	//     description: response when creating a variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	opt := web.GetForm(ctx).(*api.CreateVariableOption)
+
+	ownerID := ctx.Doer.ID
+	variableName := ctx.Params("variablename")
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ownerID,
+		Name:    variableName,
+	})
+	if err != nil && !errors.Is(err, util.ErrNotExist) {
+		ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		return
+	}
+	if v != nil && v.ID > 0 {
+		ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
+		return
+	}
+
+	if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "CreateVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// UpdateVariable update a user-level variable which is created by current doer
+func UpdateVariable(ctx *context.APIContext) {
+	// swagger:operation PUT /user/actions/variables/{variablename} user updateUserVariable
+	// ---
+	// summary: Update a user-level variable which is created by current doer
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/UpdateVariableOption"
+	// responses:
+	//   "201":
+	//     description: response when updating a variable
+	//   "204":
+	//     description: response when updating a variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	opt := web.GetForm(ctx).(*api.UpdateVariableOption)
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ctx.Doer.ID,
+		Name:    ctx.Params("variablename"),
+	})
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "GetVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		}
+		return
+	}
+
+	if opt.Name == "" {
+		opt.Name = ctx.Params("variablename")
+	}
+	if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// DeleteVariable delete a user-level variable which is created by current doer
+func DeleteVariable(ctx *context.APIContext) {
+	// swagger:operation DELETE /user/actions/variables/{variablename} user deleteUserVariable
+	// ---
+	// summary: Delete a user-level variable which is created by current doer
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// responses:
+	//   "201":
+	//     description: response when deleting a variable
+	//   "204":
+	//     description: response when deleting a variable
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	if err := actions_service.DeleteVariableByName(ctx, ctx.Doer.ID, 0, ctx.Params("variablename")); err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
+		} else if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
+		}
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
+
+// GetVariable get a user-level variable which is created by current doer
+func GetVariable(ctx *context.APIContext) {
+	// swagger:operation GET /user/actions/variables/{variablename} user getUserVariable
+	// ---
+	// summary: Get a user-level variable which is created by current doer
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: variablename
+	//   in: path
+	//   description: name of the variable
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//			"$ref": "#/responses/ActionVariable"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ctx.Doer.ID,
+		Name:    ctx.Params("variablename"),
+	})
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "GetVariable", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+		}
+		return
+	}
+
+	variable := &api.ActionVariable{
+		OwnerID: v.OwnerID,
+		RepoID:  v.RepoID,
+		Name:    v.Name,
+		Data:    v.Data,
+	}
+
+	ctx.JSON(http.StatusOK, variable)
+}
+
+// ListVariables list user-level variables
+func ListVariables(ctx *context.APIContext) {
+	// swagger:operation GET /user/actions/variables user getUserVariablesList
+	// ---
+	// summary: Get the user-level list of variables which is created by current doer
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results
+	//   type: integer
+	// responses:
+	//   "200":
+	//			"$ref": "#/responses/VariableList"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
+		OwnerID:     ctx.Doer.ID,
+		ListOptions: utils.GetListOptions(ctx),
+	})
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "FindVariables", err)
+		return
+	}
+
+	variables := make([]*api.ActionVariable, len(vars))
+	for i, v := range vars {
+		variables[i] = &api.ActionVariable{
+			OwnerID: v.OwnerID,
+			RepoID:  v.RepoID,
+			Name:    v.Name,
+			Data:    v.Data,
+		}
+	}
+
+	ctx.SetTotalCountHeader(count)
+	ctx.JSON(http.StatusOK, variables)
+}
diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go
index f045fb4d5d..88e314ed31 100644
--- a/routers/api/v1/user/app.go
+++ b/routers/api/v1/user/app.go
@@ -13,10 +13,10 @@ import (
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/user/avatar.go b/routers/api/v1/user/avatar.go
index 1c1bb6181a..f912296228 100644
--- a/routers/api/v1/user/avatar.go
+++ b/routers/api/v1/user/avatar.go
@@ -7,9 +7,9 @@ import (
 	"encoding/base64"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	user_service "code.gitea.io/gitea/services/user"
 )
 
diff --git a/routers/api/v1/user/block.go b/routers/api/v1/user/block.go
new file mode 100644
index 0000000000..7231e9add7
--- /dev/null
+++ b/routers/api/v1/user/block.go
@@ -0,0 +1,96 @@
+// Copyright 2024 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"code.gitea.io/gitea/routers/api/v1/shared"
+	"code.gitea.io/gitea/services/context"
+)
+
+func ListBlocks(ctx *context.APIContext) {
+	// swagger:operation GET /user/blocks user userListBlocks
+	// ---
+	// summary: List users blocked by the authenticated user
+	// parameters:
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results
+	//   type: integer
+	// produces:
+	// - application/json
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/UserList"
+
+	shared.ListBlocks(ctx, ctx.Doer)
+}
+
+func CheckUserBlock(ctx *context.APIContext) {
+	// swagger:operation GET /user/blocks/{username} user userCheckUserBlock
+	// ---
+	// summary: Check if a user is blocked by the authenticated user
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: user to check
+	//   type: string
+	//   required: true
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	shared.CheckUserBlock(ctx, ctx.Doer)
+}
+
+func BlockUser(ctx *context.APIContext) {
+	// swagger:operation PUT /user/blocks/{username} user userBlockUser
+	// ---
+	// summary: Block a user
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: user to block
+	//   type: string
+	//   required: true
+	// - name: note
+	//   in: query
+	//   description: optional note for the block
+	//   type: string
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+
+	shared.BlockUser(ctx, ctx.Doer)
+}
+
+func UnblockUser(ctx *context.APIContext) {
+	// swagger:operation DELETE /user/blocks/{username} user userUnblockUser
+	// ---
+	// summary: Unblock a user
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: user to unblock
+	//   type: string
+	//   required: true
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+
+	shared.UnblockUser(ctx, ctx.Doer, ctx.Doer)
+}
diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go
index 3dcea9083c..33aa851a80 100644
--- a/routers/api/v1/user/email.go
+++ b/routers/api/v1/user/email.go
@@ -8,9 +8,9 @@ import (
 	"net/http"
 
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	user_service "code.gitea.io/gitea/services/user"
 )
diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go
index 5815ed4f0b..6abb70de19 100644
--- a/routers/api/v1/user/follower.go
+++ b/routers/api/v1/user/follower.go
@@ -5,12 +5,13 @@
 package user
 
 import (
+	"errors"
 	"net/http"
 
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -221,11 +222,17 @@ func Follow(ctx *context.APIContext) {
 	// responses:
 	//   "204":
 	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
-		ctx.Error(http.StatusInternalServerError, "FollowUser", err)
+	if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil {
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "FollowUser", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "FollowUser", err)
+		}
 		return
 	}
 	ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go
index 234da5dfdc..5a2f995e1b 100644
--- a/routers/api/v1/user/gpg_key.go
+++ b/routers/api/v1/user/gpg_key.go
@@ -10,10 +10,12 @@ import (
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -132,6 +134,11 @@ func GetGPGKey(ctx *context.APIContext) {
 
 // CreateUserGPGKey creates new GPG key to given user by ID.
 func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) {
+	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
+		ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+		return
+	}
+
 	token := asymkey_model.VerificationToken(ctx.Doer, 1)
 	lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
 
@@ -268,6 +275,11 @@ func DeleteGPGKey(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
+	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
+		ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+		return
+	}
+
 	if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.ParamsInt64(":id")); err != nil {
 		if asymkey_model.IsErrGPGKeyAccessDenied(err) {
 			ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go
index 392b266ebd..8b5c64e291 100644
--- a/routers/api/v1/user/helper.go
+++ b/routers/api/v1/user/helper.go
@@ -7,7 +7,7 @@ import (
 	"net/http"
 
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 // GetUserByParamsName get user by name
diff --git a/routers/api/v1/user/hook.go b/routers/api/v1/user/hook.go
index e87385e4a2..9d9ca5bf01 100644
--- a/routers/api/v1/user/hook.go
+++ b/routers/api/v1/user/hook.go
@@ -6,10 +6,10 @@ package user
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	webhook_service "code.gitea.io/gitea/services/webhook"
 )
 
diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go
index dd185aa7d6..d9456e7ec6 100644
--- a/routers/api/v1/user/key.go
+++ b/routers/api/v1/user/key.go
@@ -5,19 +5,20 @@ package user
 
 import (
 	std_ctx "context"
+	"fmt"
 	"net/http"
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/repo"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -198,6 +199,11 @@ func GetPublicKey(ctx *context.APIContext) {
 
 // CreateUserPublicKey creates new public key to given user by ID.
 func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) {
+	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
+		ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+		return
+	}
+
 	content, err := asymkey_model.CheckPublicKeyString(form.Key)
 	if err != nil {
 		repo.HandleCheckKeyStringError(ctx, err)
@@ -263,6 +269,11 @@ func DeletePublicKey(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
+	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
+		ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+		return
+	}
+
 	id := ctx.ParamsInt64(":id")
 	externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, id)
 	if err != nil {
diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go
index b8b2d265bf..81f8e0f3fe 100644
--- a/routers/api/v1/user/repo.go
+++ b/routers/api/v1/user/repo.go
@@ -11,9 +11,9 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/user/runners.go b/routers/api/v1/user/runners.go
index 51556ae0fb..899218473e 100644
--- a/routers/api/v1/user/runners.go
+++ b/routers/api/v1/user/runners.go
@@ -4,8 +4,8 @@
 package user
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/shared"
+	"code.gitea.io/gitea/services/context"
 )
 
 // https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
diff --git a/routers/api/v1/user/settings.go b/routers/api/v1/user/settings.go
index 062df1ca43..d0a8daaa85 100644
--- a/routers/api/v1/user/settings.go
+++ b/routers/api/v1/user/settings.go
@@ -6,10 +6,10 @@ package user
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/optional"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	user_service "code.gitea.io/gitea/services/user"
 )
diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go
index 2659789ddd..ad9ed9548d 100644
--- a/routers/api/v1/user/star.go
+++ b/routers/api/v1/user/star.go
@@ -5,23 +5,26 @@
 package user
 
 import (
-	std_context "context"
+	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/models/db"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
 // getStarredRepos returns the repos that the user with the specified userID has
 // starred
-func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) {
-	starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions)
+func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
+	starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
+		ListOptions:    utils.GetListOptions(ctx),
+		StarrerID:      user.ID,
+		IncludePrivate: private,
+	})
 	if err != nil {
 		return nil, err
 	}
@@ -65,7 +68,7 @@ func GetStarredRepos(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 
 	private := ctx.ContextUser.ID == ctx.Doer.ID
-	repos, err := getStarredRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
+	repos, err := getStarredRepos(ctx, ctx.ContextUser, private)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
 		return
@@ -95,7 +98,7 @@ func GetMyStarredRepos(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/RepositoryList"
 
-	repos, err := getStarredRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
+	repos, err := getStarredRepos(ctx, ctx.Doer, true)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
 	}
@@ -152,12 +155,18 @@ func Star(ctx *context.APIContext) {
 	// responses:
 	//   "204":
 	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+	err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "StarRepo", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "BlockedUser", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "StarRepo", err)
+		}
 		return
 	}
 	ctx.Status(http.StatusNoContent)
@@ -185,7 +194,7 @@ func Unstar(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+	err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "StarRepo", err)
 		return
diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go
index fb8f67d072..09147cd2ae 100644
--- a/routers/api/v1/user/user.go
+++ b/routers/api/v1/user/user.go
@@ -9,8 +9,8 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go
index 7f531eafaa..2cc23ae476 100644
--- a/routers/api/v1/user/watch.go
+++ b/routers/api/v1/user/watch.go
@@ -4,22 +4,25 @@
 package user
 
 import (
-	std_context "context"
+	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/models/db"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
 // getWatchedRepos returns the repos that the user with the specified userID is watching
-func getWatchedRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) {
-	watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, user.ID, private, listOptions)
+func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
+	watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
+		ListOptions:    utils.GetListOptions(ctx),
+		WatcherID:      user.ID,
+		IncludePrivate: private,
+	})
 	if err != nil {
 		return nil, 0, err
 	}
@@ -63,7 +66,7 @@ func GetWatchedRepos(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 
 	private := ctx.ContextUser.ID == ctx.Doer.ID
-	repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
+	repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
 	}
@@ -92,7 +95,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/RepositoryList"
 
-	repos, total, err := getWatchedRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
+	repos, total, err := getWatchedRepos(ctx, ctx.Doer, true)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
 	}
@@ -157,12 +160,18 @@ func Watch(ctx *context.APIContext) {
 	// responses:
 	//   "200":
 	//     "$ref": "#/responses/WatchInfo"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+	err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Error(http.StatusForbidden, "BlockedUser", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
+		}
 		return
 	}
 	ctx.JSON(http.StatusOK, api.WatchInfo{
@@ -197,7 +206,7 @@ func Unwatch(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+	err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err)
 		return
diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go
index 2299cdc247..4e25137817 100644
--- a/routers/api/v1/utils/git.go
+++ b/routers/api/v1/utils/git.go
@@ -8,10 +8,10 @@ import (
 	"fmt"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 // ResolveRefOrSha resolve ref to sha if exist
@@ -72,7 +72,7 @@ func searchRefCommitByType(ctx *context.APIContext, refType, filter string) (str
 
 // ConvertToObjectID returns a full-length SHA1 from a potential ID string
 func ConvertToObjectID(ctx gocontext.Context, repo *context.Repository, commitID string) (git.ObjectID, error) {
-	objectFormat, _ := repo.GitRepo.GetObjectFormat()
+	objectFormat := repo.GetObjectFormat()
 	if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) {
 		sha, err := git.NewIDFromString(commitID)
 		if err == nil {
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index 28b21ab8db..f1abd49a7d 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -12,12 +12,12 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/models/webhook"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
+	"code.gitea.io/gitea/services/context"
 	webhook_service "code.gitea.io/gitea/services/webhook"
 )
 
diff --git a/routers/api/v1/utils/page.go b/routers/api/v1/utils/page.go
index 6910b82931..024ba7b8d9 100644
--- a/routers/api/v1/utils/page.go
+++ b/routers/api/v1/utils/page.go
@@ -5,7 +5,7 @@ package utils
 
 import (
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/common/auth.go b/routers/common/auth.go
index 8904785d51..115d65ed10 100644
--- a/routers/common/auth.go
+++ b/routers/common/auth.go
@@ -5,9 +5,9 @@ package common
 
 import (
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
 	auth_service "code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/context"
 )
 
 type AuthResult struct {
diff --git a/routers/common/errpage.go b/routers/common/errpage.go
index 923421a29c..402ca44c12 100644
--- a/routers/common/errpage.go
+++ b/routers/common/errpage.go
@@ -9,13 +9,13 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/httpcache"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/modules/web/routing"
+	"code.gitea.io/gitea/services/context"
 )
 
 const tplStatus500 base.TplName = "status/500"
diff --git a/routers/common/markup.go b/routers/common/markup.go
index a1c2c37ac0..2d5638ef61 100644
--- a/routers/common/markup.go
+++ b/routers/common/markup.go
@@ -9,11 +9,11 @@ import (
 	"net/http"
 	"strings"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 
 	"mvdan.cc/xurls/v2"
 )
@@ -34,7 +34,8 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
 		if err := markdown.RenderRaw(&markup.RenderContext{
 			Ctx: ctx,
 			Links: markup.Links{
-				Base: urlPrefix,
+				AbsolutePrefix: true,
+				Base:           urlPrefix,
 			},
 		}, strings.NewReader(text), ctx.Resp); err != nil {
 			ctx.Error(http.StatusInternalServerError, err.Error())
@@ -79,7 +80,8 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
 	if err := markup.Render(&markup.RenderContext{
 		Ctx: ctx,
 		Links: markup.Links{
-			Base: urlPrefix,
+			AbsolutePrefix: true,
+			Base:           urlPrefix,
 		},
 		Metas:        meta,
 		IsWiki:       wiki,
diff --git a/routers/common/middleware.go b/routers/common/middleware.go
index 8a39dda179..c7c75fb099 100644
--- a/routers/common/middleware.go
+++ b/routers/common/middleware.go
@@ -9,11 +9,11 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/cache"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/modules/web/routing"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/session"
 	"github.com/chi-middleware/proxy"
@@ -38,6 +38,7 @@ func ProtocolMiddlewares() (handlers []any) {
 		})
 	})
 
+	// wrap the request and response, use the process context and add it to the process manager
 	handlers = append(handlers, func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
 			ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true)
diff --git a/routers/common/redirect.go b/routers/common/redirect.go
index 9bf2025e19..34044e814b 100644
--- a/routers/common/redirect.go
+++ b/routers/common/redirect.go
@@ -17,7 +17,7 @@ func FetchRedirectDelegate(resp http.ResponseWriter, req *http.Request) {
 	// The typical page is "issue comment" page. The backend responds "/owner/repo/issues/1#comment-2",
 	// then frontend needs this delegate to redirect to the new location with hash correctly.
 	redirect := req.PostFormValue("redirect")
-	if httplib.IsRiskyRedirectURL(redirect) {
+	if !httplib.IsCurrentGiteaSiteURL(redirect) {
 		resp.WriteHeader(http.StatusBadRequest)
 		return
 	}
diff --git a/routers/common/serve.go b/routers/common/serve.go
index 8a7f8b3332..446908db75 100644
--- a/routers/common/serve.go
+++ b/routers/common/serve.go
@@ -7,11 +7,11 @@ import (
 	"io"
 	"time"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/httpcache"
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 // ServeBlob download a git.Blob
diff --git a/routers/init.go b/routers/init.go
index e0a7150ba3..aaf95920c2 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -9,7 +9,6 @@ import (
 	"runtime"
 
 	"code.gitea.io/gitea/models"
-	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	authmodel "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/modules/cache"
 	"code.gitea.io/gitea/modules/eventsource"
@@ -33,6 +32,7 @@ import (
 	"code.gitea.io/gitea/routers/private"
 	web_routers "code.gitea.io/gitea/routers/web"
 	actions_service "code.gitea.io/gitea/services/actions"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	"code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
 	"code.gitea.io/gitea/services/automerge"
@@ -94,7 +94,7 @@ func syncAppConfForGit(ctx context.Context) error {
 		mustInitCtx(ctx, repo_service.SyncRepositoryHooks)
 
 		log.Info("re-write ssh public keys ...")
-		mustInitCtx(ctx, asymkey_model.RewriteAllPublicKeys)
+		mustInitCtx(ctx, asymkey_service.RewriteAllPublicKeys)
 
 		return system.AppState.Set(ctx, runtimeState)
 	}
@@ -198,6 +198,8 @@ func NormalRoutes() *web.Route {
 		// TODO: this prefix should be generated with a token string with runner ?
 		prefix = "/api/actions_pipeline"
 		r.Mount(prefix, actions_router.ArtifactsRoutes(prefix))
+		prefix = actions_router.ArtifactV4RouteBase
+		r.Mount(prefix, actions_router.ArtifactsV4Routes(prefix))
 	}
 
 	return r
diff --git a/routers/install/install.go b/routers/install/install.go
index 5c0290d2cc..9c6a8849b6 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -7,6 +7,7 @@ package install
 import (
 	"fmt"
 	"net/http"
+	"net/mail"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -21,20 +22,20 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password/hash"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/generate"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/modules/user"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/routers/common"
 	auth_service "code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 
 	"gitea.com/go-chi/session"
@@ -409,7 +410,7 @@ func SubmitInstall(ctx *context.Context) {
 		cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
 		cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath)
 		var lfsJwtSecret string
-		if _, lfsJwtSecret, err = generate.NewJwtSecretBase64(); err != nil {
+		if _, lfsJwtSecret, err = generate.NewJwtSecretWithBase64(); err != nil {
 			ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form)
 			return
 		}
@@ -419,6 +420,11 @@ func SubmitInstall(ctx *context.Context) {
 	}
 
 	if len(strings.TrimSpace(form.SMTPAddr)) > 0 {
+		if _, err := mail.ParseAddress(form.SMTPFrom); err != nil {
+			ctx.RenderWithErr(ctx.Tr("install.smtp_from_invalid"), tplInstall, &form)
+			return
+		}
+
 		cfg.Section("mailer").Key("ENABLED").SetValue("true")
 		cfg.Section("mailer").Key("SMTP_ADDR").SetValue(form.SMTPAddr)
 		cfg.Section("mailer").Key("SMTP_PORT").SetValue(form.SMTPPort)
@@ -533,8 +539,8 @@ func SubmitInstall(ctx *context.Context) {
 			IsAdmin: true,
 		}
 		overwriteDefault := &user_model.CreateUserOverwriteOptions{
-			IsRestricted: util.OptionalBoolFalse,
-			IsActive:     util.OptionalBoolTrue,
+			IsRestricted: optional.Some(false),
+			IsActive:     optional.Some(true),
 		}
 
 		if err = user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
diff --git a/routers/private/actions.go b/routers/private/actions.go
index 886f23b1c2..696634b5e7 100644
--- a/routers/private/actions.go
+++ b/routers/private/actions.go
@@ -12,11 +12,11 @@ import (
 	actions_model "code.gitea.io/gitea/models/actions"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 // GenerateActionsRunnerToken generates a new runner token for a given scope
@@ -26,7 +26,7 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
 	defer rd.Close()
 
 	if err := json.NewDecoder(rd).Decode(&genRequest); err != nil {
-		log.Error("%v", err)
+		log.Error("JSON Decode failed: %v", err)
 		ctx.JSON(http.StatusInternalServerError, private.Response{
 			Err: err.Error(),
 		})
@@ -35,7 +35,7 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
 
 	owner, repo, err := parseScope(ctx, genRequest.Scope)
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("parseScope failed: %v", err)
 		ctx.JSON(http.StatusInternalServerError, private.Response{
 			Err: err.Error(),
 		})
@@ -45,18 +45,18 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
 	if errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) {
 		token, err = actions_model.NewRunnerToken(ctx, owner, repo)
 		if err != nil {
-			err := fmt.Sprintf("error while creating runner token: %v", err)
-			log.Error("%v", err)
+			errMsg := fmt.Sprintf("error while creating runner token: %v", err)
+			log.Error("NewRunnerToken failed: %v", errMsg)
 			ctx.JSON(http.StatusInternalServerError, private.Response{
-				Err: err,
+				Err: errMsg,
 			})
 			return
 		}
 	} else if err != nil {
-		err := fmt.Sprintf("could not get unactivated runner token: %v", err)
-		log.Error("%v", err)
+		errMsg := fmt.Sprintf("could not get unactivated runner token: %v", err)
+		log.Error("GetLatestRunnerToken failed: %v", errMsg)
 		ctx.JSON(http.StatusInternalServerError, private.Response{
-			Err: err,
+			Err: errMsg,
 		})
 		return
 	}
diff --git a/routers/private/default_branch.go b/routers/private/default_branch.go
index a23e101e9d..33890be6a9 100644
--- a/routers/private/default_branch.go
+++ b/routers/private/default_branch.go
@@ -8,9 +8,10 @@ import (
 	"net/http"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/private"
+	gitea_context "code.gitea.io/gitea/services/context"
 )
 
 // SetDefaultBranch updates the default branch
@@ -20,7 +21,7 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) {
 	branch := ctx.Params(":branch")
 
 	ctx.Repo.Repository.DefaultBranch = branch
-	if err := ctx.Repo.GitRepo.SetDefaultBranch(ctx.Repo.Repository.DefaultBranch); err != nil {
+	if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
 		if !git.IsErrUnsupportedVersion(err) {
 			ctx.JSON(http.StatusInternalServerError, private.Response{
 				Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index 1b274ae154..769a68970d 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -6,18 +6,22 @@ package private
 import (
 	"fmt"
 	"net/http"
-	"strconv"
 
+	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
+	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
-	gitea_context "code.gitea.io/gitea/modules/context"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	gitea_context "code.gitea.io/gitea/services/context"
+	pull_service "code.gitea.io/gitea/services/pull"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
@@ -27,6 +31,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 
 	// We don't rely on RepoAssignment here because:
 	// a) we don't need the git repo in this function
+	//    OUT OF DATE: we do need the git repo to sync the branch to the db now.
 	// b) our update function will likely change the repository in the db so we will need to refresh it
 	// c) we don't always need the repo
 
@@ -34,7 +39,11 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 	repoName := ctx.Params(":repo")
 
 	// defer getting the repository at this point - as we should only retrieve it if we're going to call update
-	var repo *repo_model.Repository
+	var (
+		repo    *repo_model.Repository
+		gitRepo *git.Repository
+	)
+	defer gitRepo.Close() // it's safe to call Close on a nil pointer
 
 	updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs))
 	wasEmpty := false
@@ -68,6 +77,10 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 			updates = append(updates, option)
 			if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") {
 				// put the master/main branch first
+				// FIXME: It doesn't always work, since the master/main branch may not be the first batch of updates.
+				//        If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once.
+				//        See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27
+				//        If the user executes `git push origin --all` and pushes more than 30 branches, the master/main may not be the default branch.
 				copy(updates[1:], updates)
 				updates[0] = option
 			}
@@ -75,6 +88,64 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 	}
 
 	if repo != nil && len(updates) > 0 {
+		branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates))
+		for _, update := range updates {
+			if !update.RefFullName.IsBranch() {
+				continue
+			}
+			if repo == nil {
+				repo = loadRepository(ctx, ownerName, repoName)
+				if ctx.Written() {
+					return
+				}
+				wasEmpty = repo.IsEmpty
+			}
+
+			if update.IsDelRef() {
+				if err := git_model.AddDeletedBranch(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil {
+					log.Error("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err)
+					ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+						Err: fmt.Sprintf("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err),
+					})
+					return
+				}
+			} else {
+				branchesToSync = append(branchesToSync, update)
+
+				// TODO: should we return the error and return the error when pushing? Currently it will log the error and not prevent the pushing
+				pull_service.UpdatePullsRefs(ctx, repo, update)
+			}
+		}
+		if len(branchesToSync) > 0 {
+			if gitRepo == nil {
+				var err error
+				gitRepo, err = gitrepo.OpenRepository(ctx, repo)
+				if err != nil {
+					log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
+					ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+						Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
+					})
+					return
+				}
+			}
+
+			var (
+				branchNames = make([]string, 0, len(branchesToSync))
+				commitIDs   = make([]string, 0, len(branchesToSync))
+			)
+			for _, update := range branchesToSync {
+				branchNames = append(branchNames, update.RefFullName.BranchName())
+				commitIDs = append(commitIDs, update.NewCommitID)
+			}
+
+			if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, gitRepo.GetCommit); err != nil {
+				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+					Err: fmt.Sprintf("Failed to sync branch to DB in repository: %s/%s Error: %v", ownerName, repoName, err),
+				})
+				return
+			}
+		}
+
 		if err := repo_service.PushUpdates(updates); err != nil {
 			log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
 			for i, update := range updates {
@@ -89,8 +160,10 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 		}
 	}
 
+	isPrivate := opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate)
+	isTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate)
 	// Handle Push Options
-	if len(opts.GitPushOptions) > 0 {
+	if isPrivate.Has() || isTemplate.Has() {
 		// load the repository
 		if repo == nil {
 			repo = loadRepository(ctx, ownerName, repoName)
@@ -101,13 +174,49 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 			wasEmpty = repo.IsEmpty
 		}
 
-		repo.IsPrivate = opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate, repo.IsPrivate)
-		repo.IsTemplate = opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate, repo.IsTemplate)
-		if err := repo_model.UpdateRepositoryCols(ctx, repo, "is_private", "is_template"); err != nil {
+		pusher, err := user_model.GetUserByID(ctx, opts.UserID)
+		if err != nil {
 			log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
 			ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
 				Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
 			})
+			return
+		}
+		perm, err := access_model.GetUserRepoPermission(ctx, repo, pusher)
+		if err != nil {
+			log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
+			ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+				Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
+			})
+			return
+		}
+		if !perm.IsOwner() && !perm.IsAdmin() {
+			ctx.JSON(http.StatusNotFound, private.HookPostReceiveResult{
+				Err: "Permissions denied",
+			})
+			return
+		}
+
+		cols := make([]string, 0, len(opts.GitPushOptions))
+
+		if isPrivate.Has() {
+			repo.IsPrivate = isPrivate.Value()
+			cols = append(cols, "is_private")
+		}
+
+		if isTemplate.Has() {
+			repo.IsTemplate = isTemplate.Value()
+			cols = append(cols, "is_template")
+		}
+
+		if len(cols) > 0 {
+			if err := repo_model.UpdateRepositoryCols(ctx, repo, cols...); err != nil {
+				log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
+				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+					Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
+				})
+				return
+			}
 		}
 	}
 
@@ -122,44 +231,8 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 		refFullName := opts.RefFullNames[i]
 		newCommitID := opts.NewCommitIDs[i]
 
-		// post update for agit pull request
-		// FIXME: use pr.Flow to test whether it's an Agit PR or a GH PR
-		if git.SupportProcReceive && refFullName.IsPull() {
-			if repo == nil {
-				repo = loadRepository(ctx, ownerName, repoName)
-				if ctx.Written() {
-					return
-				}
-			}
-
-			pullIndex, _ := strconv.ParseInt(refFullName.PullName(), 10, 64)
-			if pullIndex <= 0 {
-				continue
-			}
-
-			pr, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, pullIndex)
-			if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
-				log.Error("Failed to get PR by index %v Error: %v", pullIndex, err)
-				ctx.JSON(http.StatusInternalServerError, private.Response{
-					Err: fmt.Sprintf("Failed to get PR by index %v Error: %v", pullIndex, err),
-				})
-				return
-			}
-			if pr == nil {
-				continue
-			}
-
-			results = append(results, private.HookPostReceiveBranchResult{
-				Message: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
-				Create:  false,
-				Branch:  "",
-				URL:     fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
-			})
-			continue
-		}
-
 		// If we've pushed a branch (and not deleted it)
-		if git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() {
+		if !git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() {
 			// First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo
 			if repo == nil {
 				repo = loadRepository(ctx, ownerName, repoName)
diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go
index 90d8287f06..32ec3003e2 100644
--- a/routers/private/hook_pre_receive.go
+++ b/routers/private/hook_pre_receive.go
@@ -16,11 +16,11 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/web"
+	gitea_context "code.gitea.io/gitea/services/context"
 	pull_service "code.gitea.io/gitea/services/pull"
 )
 
@@ -122,7 +122,7 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
 			preReceiveBranch(ourCtx, oldCommitID, newCommitID, refFullName)
 		case refFullName.IsTag():
 			preReceiveTag(ourCtx, oldCommitID, newCommitID, refFullName)
-		case git.SupportProcReceive && refFullName.IsFor():
+		case git.DefaultFeatures.SupportProcReceive && refFullName.IsFor():
 			preReceiveFor(ourCtx, oldCommitID, newCommitID, refFullName)
 		default:
 			ourCtx.AssertCanWriteCode()
@@ -145,7 +145,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
 
 	repo := ctx.Repo.Repository
 	gitRepo := ctx.Repo.GitRepo
-	objectFormat, _ := gitRepo.GetObjectFormat()
+	objectFormat := ctx.Repo.GetObjectFormat()
 
 	if branchName == repo.DefaultBranch && newCommitID == objectFormat.EmptyObjectID().String() {
 		log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo)
diff --git a/routers/private/hook_proc_receive.go b/routers/private/hook_proc_receive.go
index 5577120770..cee3bbdd12 100644
--- a/routers/private/hook_proc_receive.go
+++ b/routers/private/hook_proc_receive.go
@@ -7,18 +7,18 @@ import (
 	"net/http"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/agit"
+	gitea_context "code.gitea.io/gitea/services/context"
 )
 
 // HookProcReceive proc-receive hook - only handles agit Proc-Receive requests at present
 func HookProcReceive(ctx *gitea_context.PrivateContext) {
 	opts := web.GetForm(ctx).(*private.HookOptions)
-	if !git.SupportProcReceive {
+	if !git.DefaultFeatures.SupportProcReceive {
 		ctx.Status(http.StatusNotFound)
 		return
 	}
diff --git a/routers/private/hook_verification.go b/routers/private/hook_verification.go
index 42b8e5abed..764c976fa9 100644
--- a/routers/private/hook_verification.go
+++ b/routers/private/hook_verification.go
@@ -47,7 +47,7 @@ func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []
 			_ = stdoutWriter.Close()
 			err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env)
 			if err != nil {
-				log.Error("%v", err)
+				log.Error("readAndVerifyCommitsFromShaReader failed: %v", err)
 				cancel()
 			}
 			_ = stdoutReader.Close()
@@ -66,7 +66,6 @@ func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository
 		line := scanner.Text()
 		err := readAndVerifyCommit(line, repo, env)
 		if err != nil {
-			log.Error("%v", err)
 			return err
 		}
 	}
diff --git a/routers/private/internal.go b/routers/private/internal.go
index 407edebeed..ede310113c 100644
--- a/routers/private/internal.go
+++ b/routers/private/internal.go
@@ -8,11 +8,11 @@ import (
 	"net/http"
 	"strings"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 	chi_middleware "github.com/go-chi/chi/v5/middleware"
diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go
index 615239d479..e8ee8ba8ac 100644
--- a/routers/private/internal_repo.go
+++ b/routers/private/internal_repo.go
@@ -9,10 +9,10 @@ import (
 	"net/http"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
+	gitea_context "code.gitea.io/gitea/services/context"
 )
 
 // This file contains common functions relating to setting the Repository for the internal routes
diff --git a/routers/private/key.go b/routers/private/key.go
index 0096480d6a..5b8f238a83 100644
--- a/routers/private/key.go
+++ b/routers/private/key.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/services/context"
 )
 
 // UpdatePublicKeyInRepo update public key and deploy key updates
diff --git a/routers/private/mail.go b/routers/private/mail.go
index e5e162c880..cf3abb31c6 100644
--- a/routers/private/mail.go
+++ b/routers/private/mail.go
@@ -11,11 +11,11 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/mailer"
 )
 
@@ -35,7 +35,7 @@ func SendEmail(ctx *context.PrivateContext) {
 	defer rd.Close()
 
 	if err := json.NewDecoder(rd).Decode(&mail); err != nil {
-		log.Error("%v", err)
+		log.Error("JSON Decode failed: %v", err)
 		ctx.JSON(http.StatusInternalServerError, private.Response{
 			Err: err.Error(),
 		})
diff --git a/routers/private/manager.go b/routers/private/manager.go
index 397e6fac7b..a6aa03e4ec 100644
--- a/routers/private/manager.go
+++ b/routers/private/manager.go
@@ -8,7 +8,6 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/graceful/releasereopen"
 	"code.gitea.io/gitea/modules/log"
@@ -17,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 )
 
 // ReloadTemplates reloads all the templates
diff --git a/routers/private/manager_process.go b/routers/private/manager_process.go
index 68e4a21805..9a0298a37c 100644
--- a/routers/private/manager_process.go
+++ b/routers/private/manager_process.go
@@ -11,10 +11,10 @@ import (
 	"runtime"
 	"time"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	process_module "code.gitea.io/gitea/modules/process"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Processes prints out the processes
diff --git a/routers/private/manager_unix.go b/routers/private/manager_unix.go
index 09ced33b8d..0c63ebc918 100644
--- a/routers/private/manager_unix.go
+++ b/routers/private/manager_unix.go
@@ -8,8 +8,8 @@ package private
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/graceful"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Restart causes the server to perform a graceful restart
diff --git a/routers/private/manager_windows.go b/routers/private/manager_windows.go
index bd3c3c30d0..f1b9365f52 100644
--- a/routers/private/manager_windows.go
+++ b/routers/private/manager_windows.go
@@ -8,9 +8,9 @@ package private
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/private"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Restart is not implemented for Windows based servers as they can't fork
diff --git a/routers/private/restore_repo.go b/routers/private/restore_repo.go
index 7efc22a3d9..4e95d3071d 100644
--- a/routers/private/restore_repo.go
+++ b/routers/private/restore_repo.go
@@ -7,9 +7,9 @@ import (
 	"io"
 	"net/http"
 
-	myCtx "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/private"
+	myCtx "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/migrations"
 )
 
diff --git a/routers/private/serv.go b/routers/private/serv.go
index 00731947a5..85368a0aed 100644
--- a/routers/private/serv.go
+++ b/routers/private/serv.go
@@ -14,11 +14,11 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 	wiki_service "code.gitea.io/gitea/services/wiki"
 )
@@ -297,7 +297,7 @@ func ServCommand(ctx *context.PrivateContext) {
 			}
 		} else {
 			// Because of the special ref "refs/for" we will need to delay write permission check
-			if git.SupportProcReceive && unitType == unit.TypeCode {
+			if git.DefaultFeatures.SupportProcReceive && unitType == unit.TypeCode {
 				mode = perm.AccessModeRead
 			}
 
diff --git a/routers/private/ssh_log.go b/routers/private/ssh_log.go
index eacfa18f05..5bec632ead 100644
--- a/routers/private/ssh_log.go
+++ b/routers/private/ssh_log.go
@@ -6,11 +6,11 @@ package private
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 )
 
 // SSHLog hook to response ssh log
diff --git a/routers/utils/utils.go b/routers/utils/utils.go
index 1f4d11fd3c..3035073d5c 100644
--- a/routers/utils/utils.go
+++ b/routers/utils/utils.go
@@ -5,26 +5,10 @@ package utils
 
 import (
 	"html"
-	"net/url"
 	"strings"
-
-	"code.gitea.io/gitea/modules/setting"
 )
 
 // SanitizeFlashErrorString will sanitize a flash error string
 func SanitizeFlashErrorString(x string) string {
 	return strings.ReplaceAll(html.EscapeString(x), "\n", "<br>")
 }
-
-// IsExternalURL checks if rawURL points to an external URL like http://example.com
-func IsExternalURL(rawURL string) bool {
-	parsed, err := url.Parse(rawURL)
-	if err != nil {
-		return true
-	}
-	appURL, _ := url.Parse(setting.AppURL)
-	if len(parsed.Host) != 0 && strings.Replace(parsed.Host, "www.", "", 1) != strings.Replace(appURL.Host, "www.", "", 1) {
-		return true
-	}
-	return false
-}
diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go
index 440aad87c6..6e7f3c33cd 100644
--- a/routers/utils/utils_test.go
+++ b/routers/utils/utils_test.go
@@ -5,47 +5,8 @@ package utils
 
 import (
 	"testing"
-
-	"code.gitea.io/gitea/modules/setting"
-
-	"github.com/stretchr/testify/assert"
 )
 
-func TestIsExternalURL(t *testing.T) {
-	setting.AppURL = "https://try.gitea.io/"
-	type test struct {
-		Expected bool
-		RawURL   string
-	}
-	newTest := func(expected bool, rawURL string) test {
-		return test{Expected: expected, RawURL: rawURL}
-	}
-	for _, test := range []test{
-		newTest(false,
-			"https://try.gitea.io"),
-		newTest(true,
-			"https://example.com/"),
-		newTest(true,
-			"//example.com"),
-		newTest(true,
-			"http://example.com"),
-		newTest(false,
-			"a/"),
-		newTest(false,
-			"https://try.gitea.io/test?param=false"),
-		newTest(false,
-			"test?param=false"),
-		newTest(false,
-			"//try.gitea.io/test?param=false"),
-		newTest(false,
-			"/hey/hey/hey#3244"),
-		newTest(true,
-			"://missing protocol scheme"),
-	} {
-		assert.Equal(t, test.Expected, IsExternalURL(test.RawURL))
-	}
-}
-
 func TestSanitizeFlashErrorString(t *testing.T) {
 	tests := []struct {
 		name string
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index d31cb1cd25..e6585d8833 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -14,13 +14,13 @@ import (
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/updatechecker"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/cron"
 	"code.gitea.io/gitea/services/forms"
 	release_service "code.gitea.io/gitea/services/release"
@@ -28,13 +28,14 @@ import (
 )
 
 const (
-	tplDashboard   base.TplName = "admin/dashboard"
-	tplSelfCheck   base.TplName = "admin/self_check"
-	tplCron        base.TplName = "admin/cron"
-	tplQueue       base.TplName = "admin/queue"
-	tplStacktrace  base.TplName = "admin/stacktrace"
-	tplQueueManage base.TplName = "admin/queue_manage"
-	tplStats       base.TplName = "admin/stats"
+	tplDashboard    base.TplName = "admin/dashboard"
+	tplSystemStatus base.TplName = "admin/system_status"
+	tplSelfCheck    base.TplName = "admin/self_check"
+	tplCron         base.TplName = "admin/cron"
+	tplQueue        base.TplName = "admin/queue"
+	tplStacktrace   base.TplName = "admin/stacktrace"
+	tplQueueManage  base.TplName = "admin/queue_manage"
+	tplStats        base.TplName = "admin/stats"
 )
 
 var sysStatus struct {
@@ -72,7 +73,7 @@ var sysStatus struct {
 
 	// Garbage collector statistics.
 	NextGC       string // next run in HeapAlloc time (bytes)
-	LastGC       string // last run in absolute time (ns)
+	LastGCTime   string // last run time
 	PauseTotalNs string
 	PauseNs      string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
 	NumGC        uint32
@@ -110,17 +111,17 @@ func updateSystemStatus() {
 	sysStatus.OtherSys = base.FileSize(int64(m.OtherSys))
 
 	sysStatus.NextGC = base.FileSize(int64(m.NextGC))
-	sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
+	sysStatus.LastGCTime = time.Unix(0, int64(m.LastGC)).Format(time.RFC3339)
 	sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
 	sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
 	sysStatus.NumGC = m.NumGC
 }
 
-func prepareDeprecatedWarningsAlert(ctx *context.Context) {
-	if len(setting.DeprecatedWarnings) > 0 {
-		content := setting.DeprecatedWarnings[0]
-		if len(setting.DeprecatedWarnings) > 1 {
-			content += fmt.Sprintf(" (and %d more)", len(setting.DeprecatedWarnings)-1)
+func prepareStartupProblemsAlert(ctx *context.Context) {
+	if len(setting.StartupProblems) > 0 {
+		content := setting.StartupProblems[0]
+		if len(setting.StartupProblems) > 1 {
+			content += fmt.Sprintf(" (and %d more)", len(setting.StartupProblems)-1)
 		}
 		ctx.Flash.Error(content, true)
 	}
@@ -132,14 +133,19 @@ func Dashboard(ctx *context.Context) {
 	ctx.Data["PageIsAdminDashboard"] = true
 	ctx.Data["NeedUpdate"] = updatechecker.GetNeedUpdate(ctx)
 	ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion(ctx)
-	// FIXME: update periodically
 	updateSystemStatus()
 	ctx.Data["SysStatus"] = sysStatus
 	ctx.Data["SSH"] = setting.SSH
-	prepareDeprecatedWarningsAlert(ctx)
+	prepareStartupProblemsAlert(ctx)
 	ctx.HTML(http.StatusOK, tplDashboard)
 }
 
+func SystemStatus(ctx *context.Context) {
+	updateSystemStatus()
+	ctx.Data["SysStatus"] = sysStatus
+	ctx.HTML(http.StatusOK, tplSystemStatus)
+}
+
 // DashboardPost run an admin operation
 func DashboardPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.AdminDashboardForm)
@@ -184,6 +190,14 @@ func DashboardPost(ctx *context.Context) {
 
 func SelfCheck(ctx *context.Context) {
 	ctx.Data["PageIsAdminSelfCheck"] = true
+
+	ctx.Data["StartupProblems"] = setting.StartupProblems
+	if len(setting.StartupProblems) == 0 && !setting.IsProd {
+		if time.Now().Unix()%2 == 0 {
+			ctx.Data["StartupProblems"] = []string{"This is a test warning message in dev mode"}
+		}
+	}
+
 	r, err := db.CheckCollationsDefaultEngine()
 	if err != nil {
 		ctx.Flash.Error(fmt.Sprintf("CheckCollationsDefaultEngine: %v", err), true)
diff --git a/routers/web/admin/applications.go b/routers/web/admin/applications.go
index b6f7bcd2a5..8583398074 100644
--- a/routers/web/admin/applications.go
+++ b/routers/web/admin/applications.go
@@ -10,9 +10,9 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	user_setting "code.gitea.io/gitea/routers/web/user/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 var (
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 2cf63c646d..ba487d1045 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -16,7 +16,6 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/auth/pam"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
@@ -27,6 +26,7 @@ import (
 	pam_service "code.gitea.io/gitea/services/auth/source/pam"
 	"code.gitea.io/gitea/services/auth/source/smtp"
 	"code.gitea.io/gitea/services/auth/source/sspi"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 
 	"xorm.io/xorm/convert"
@@ -210,16 +210,16 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
 func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
 	if util.IsEmptyString(form.SSPISeparatorReplacement) {
 		ctx.Data["Err_SSPISeparatorReplacement"] = true
-		return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
+		return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.require_error"))
 	}
 	if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
 		ctx.Data["Err_SSPISeparatorReplacement"] = true
-		return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.alpha_dash_dot_error"))
+		return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.alpha_dash_dot_error"))
 	}
 
 	if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
 		ctx.Data["Err_SSPIDefaultLanguage"] = true
-		return nil, errors.New(ctx.Tr("form.lang_select_error"))
+		return nil, errors.New(ctx.Locale.TrString("form.lang_select_error"))
 	}
 
 	return &sspi.Source{
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
index c827f2a4f5..48f80dbbf1 100644
--- a/routers/web/admin/config.go
+++ b/routers/web/admin/config.go
@@ -7,24 +7,27 @@ package admin
 import (
 	"net/http"
 	"net/url"
+	"strconv"
 	"strings"
 
 	system_model "code.gitea.io/gitea/models/system"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/setting/config"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/mailer"
 
 	"gitea.com/go-chi/session"
 )
 
-const tplConfig base.TplName = "admin/config"
+const (
+	tplConfig         base.TplName = "admin/config"
+	tplConfigSettings base.TplName = "admin/config_settings"
+)
 
 // SendTestMail send test mail to confirm mail service is OK
 func SendTestMail(ctx *context.Context) {
@@ -98,8 +101,9 @@ func shadowPassword(provider, cfgItem string) string {
 
 // Config show admin config page
 func Config(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("admin.config")
+	ctx.Data["Title"] = ctx.Tr("admin.config_summary")
 	ctx.Data["PageIsAdminConfig"] = true
+	ctx.Data["PageIsAdminConfigSummary"] = true
 
 	ctx.Data["CustomConf"] = setting.CustomConf
 	ctx.Data["AppUrl"] = setting.AppURL
@@ -161,23 +165,70 @@ func Config(ctx *context.Context) {
 
 	ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
 	config.GetDynGetter().InvalidateCache()
-	ctx.Data["SystemConfig"] = setting.Config()
-	prepareDeprecatedWarningsAlert(ctx)
+	prepareStartupProblemsAlert(ctx)
 
 	ctx.HTML(http.StatusOK, tplConfig)
 }
 
+func ConfigSettings(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("admin.config_settings")
+	ctx.Data["PageIsAdminConfig"] = true
+	ctx.Data["PageIsAdminConfigSettings"] = true
+	ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString()
+	ctx.HTML(http.StatusOK, tplConfigSettings)
+}
+
 func ChangeConfig(ctx *context.Context) {
 	key := strings.TrimSpace(ctx.FormString("key"))
 	value := ctx.FormString("value")
 	cfg := setting.Config()
-	allowedKeys := container.SetOf(cfg.Picture.DisableGravatar.DynKey(), cfg.Picture.EnableFederatedAvatar.DynKey())
-	if !allowedKeys.Contains(key) {
+
+	marshalBool := func(v string) (string, error) {
+		if b, _ := strconv.ParseBool(v); b {
+			return "true", nil
+		}
+		return "false", nil
+	}
+	marshalOpenWithApps := func(value string) (string, error) {
+		lines := strings.Split(value, "\n")
+		var openWithEditorApps setting.OpenWithEditorAppsType
+		for _, line := range lines {
+			line = strings.TrimSpace(line)
+			if line == "" {
+				continue
+			}
+			displayName, openURL, ok := strings.Cut(line, "=")
+			displayName, openURL = strings.TrimSpace(displayName), strings.TrimSpace(openURL)
+			if !ok || displayName == "" || openURL == "" {
+				continue
+			}
+			openWithEditorApps = append(openWithEditorApps, setting.OpenWithEditorApp{
+				DisplayName: strings.TrimSpace(displayName),
+				OpenURL:     strings.TrimSpace(openURL),
+			})
+		}
+		b, err := json.Marshal(openWithEditorApps)
+		if err != nil {
+			return "", err
+		}
+		return string(b), nil
+	}
+	marshallers := map[string]func(string) (string, error){
+		cfg.Picture.DisableGravatar.DynKey():       marshalBool,
+		cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
+		cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
+	}
+	marshaller, hasMarshaller := marshallers[key]
+	if !hasMarshaller {
 		ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
 		return
 	}
-	if err := system_model.SetSettings(ctx, map[string]string{key: value}); err != nil {
-		log.Error("set setting failed: %v", err)
+	marshaledValue, err := marshaller(value)
+	if err != nil {
+		ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
+		return
+	}
+	if err = system_model.SetSettings(ctx, map[string]string{key: marshaledValue}); err != nil {
 		ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
 		return
 	}
diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go
index 2d550125d5..020554a35a 100644
--- a/routers/web/admin/diagnosis.go
+++ b/routers/web/admin/diagnosis.go
@@ -9,8 +9,8 @@ import (
 	"runtime/pprof"
 	"time"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/httplib"
+	"code.gitea.io/gitea/services/context"
 )
 
 func MonitorDiagnosis(ctx *context.Context) {
diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go
index 59f80035d8..2cf4035c6a 100644
--- a/routers/web/admin/emails.go
+++ b/routers/web/admin/emails.go
@@ -11,10 +11,10 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -68,10 +68,10 @@ func Emails(ctx *context.Context) {
 	opts.Keyword = ctx.FormTrim("q")
 	opts.SortType = orderBy
 	if len(ctx.FormString("is_activated")) != 0 {
-		opts.IsActivated = util.OptionalBoolOf(ctx.FormBool("activated"))
+		opts.IsActivated = optional.Some(ctx.FormBool("activated"))
 	}
 	if len(ctx.FormString("is_primary")) != 0 {
-		opts.IsPrimary = util.OptionalBoolOf(ctx.FormBool("primary"))
+		opts.IsPrimary = optional.Some(ctx.FormBool("primary"))
 	}
 
 	if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go
index cd8cc29cdf..8d59fbb858 100644
--- a/routers/web/admin/hooks.go
+++ b/routers/web/admin/hooks.go
@@ -8,9 +8,9 @@ import (
 
 	"code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -35,7 +35,7 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
 
 	sys["Title"] = ctx.Tr("admin.systemhooks")
 	sys["Description"] = ctx.Tr("admin.systemhooks.desc")
-	sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
+	sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, optional.None[bool]())
 	sys["BaseLink"] = setting.AppSubURL + "/admin/hooks"
 	sys["BaseLinkNew"] = setting.AppSubURL + "/admin/system-hooks"
 	if err != nil {
diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go
index e1cb578d05..36303cbc06 100644
--- a/routers/web/admin/notice.go
+++ b/routers/web/admin/notice.go
@@ -11,9 +11,9 @@ import (
 	"code.gitea.io/gitea/models/db"
 	system_model "code.gitea.io/gitea/models/system"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go
index 00131c9e2f..c5454db71e 100644
--- a/routers/web/admin/orgs.go
+++ b/routers/web/admin/orgs.go
@@ -8,10 +8,10 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/web/explore"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go
index 35ce215be4..39f064a1be 100644
--- a/routers/web/admin/packages.go
+++ b/routers/web/admin/packages.go
@@ -11,9 +11,9 @@ import (
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
 	packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
 )
@@ -36,7 +36,7 @@ func Packages(ctx *context.Context) {
 		Type:       packages_model.Type(packageType),
 		Name:       packages_model.SearchValue{Value: query},
 		Sort:       sort,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 		Paginator: &db.ListOptions{
 			PageSize: setting.UI.PackagesPagingNum,
 			Page:     page,
diff --git a/routers/web/admin/queue.go b/routers/web/admin/queue.go
index 18a8d7d3e6..d8c50730b1 100644
--- a/routers/web/admin/queue.go
+++ b/routers/web/admin/queue.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 	"strconv"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/queue"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 func Queues(ctx *context.Context) {
diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go
index 45c280ef73..0815879bb3 100644
--- a/routers/web/admin/repos.go
+++ b/routers/web/admin/repos.go
@@ -4,6 +4,7 @@
 package admin
 
 import (
+	"fmt"
 	"net/http"
 	"net/url"
 	"strings"
@@ -12,11 +13,11 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/explore"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
@@ -84,7 +85,7 @@ func UnadoptedRepos(ctx *context.Context) {
 	if !doSearch {
 		pager := context.NewPagination(0, opts.PageSize, opts.Page, 5)
 		pager.SetDefaultParams(ctx)
-		pager.AddParam(ctx, "search", "search")
+		pager.AddParamString("search", fmt.Sprint(doSearch))
 		ctx.Data["Page"] = pager
 		ctx.HTML(http.StatusOK, tplUnadoptedRepos)
 		return
@@ -98,7 +99,7 @@ func UnadoptedRepos(ctx *context.Context) {
 	ctx.Data["Dirs"] = repoNames
 	pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "search", "search")
+	pager.AddParamString("search", fmt.Sprint(doSearch))
 	ctx.Data["Page"] = pager
 	ctx.HTML(http.StatusOK, tplUnadoptedRepos)
 }
diff --git a/routers/web/admin/runners.go b/routers/web/admin/runners.go
index eaa268b4f1..d73290a8db 100644
--- a/routers/web/admin/runners.go
+++ b/routers/web/admin/runners.go
@@ -4,8 +4,8 @@
 package admin
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/admin/stacktrace.go b/routers/web/admin/stacktrace.go
index b603fb59a2..d6def94bb4 100644
--- a/routers/web/admin/stacktrace.go
+++ b/routers/web/admin/stacktrace.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 	"runtime"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Stacktrace show admin monitor goroutines page
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index af184fa9eb..ea9d6f4c9c 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -19,7 +19,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
@@ -27,6 +26,7 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/web/explore"
 	user_setting "code.gitea.io/gitea/routers/web/user/setting"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
 	user_service "code.gitea.io/gitea/services/user"
@@ -96,7 +96,7 @@ func NewUser(ctx *context.Context) {
 	ctx.Data["login_type"] = "0-0"
 
 	sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
-		IsActive: util.OptionalBoolTrue,
+		IsActive: optional.Some(true),
 	})
 	if err != nil {
 		ctx.ServerError("auth.Sources", err)
@@ -117,7 +117,7 @@ func NewUserPost(ctx *context.Context) {
 	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
 
 	sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
-		IsActive: util.OptionalBoolTrue,
+		IsActive: optional.Some(true),
 	})
 	if err != nil {
 		ctx.ServerError("auth.Sources", err)
@@ -140,7 +140,7 @@ func NewUserPost(ctx *context.Context) {
 	}
 
 	overwriteDefault := &user_model.CreateUserOverwriteOptions{
-		IsActive:   util.OptionalBoolTrue,
+		IsActive:   optional.Some(true),
 		Visibility: &form.Visibility,
 	}
 
@@ -177,7 +177,7 @@ func NewUserPost(ctx *context.Context) {
 		u.MustChangePassword = form.MustChangePassword
 	}
 
-	if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
+	if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil {
 		switch {
 		case user_model.IsErrUserAlreadyExist(err):
 			ctx.Data["Err_UserName"] = true
@@ -202,6 +202,11 @@ func NewUserPost(ctx *context.Context) {
 		}
 		return
 	}
+
+	if !user_model.IsEmailDomainAllowed(u.Email) {
+		ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email))
+	}
+
 	log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
 
 	// Send email notification.
@@ -270,13 +275,11 @@ func ViewUser(ctx *context.Context) {
 	}
 
 	repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		ListOptions: db.ListOptionsAll,
 		OwnerID:     u.ID,
 		OrderBy:     db.SearchOrderByAlphabetically,
 		Private:     true,
-		Collaborate: util.OptionalBoolFalse,
+		Collaborate: optional.Some(false),
 	})
 	if err != nil {
 		ctx.ServerError("SearchRepository", err)
@@ -295,9 +298,7 @@ func ViewUser(ctx *context.Context) {
 	ctx.Data["EmailsTotal"] = len(emails)
 
 	orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		ListOptions:    db.ListOptionsAll,
 		UserID:         u.ID,
 		IncludePrivate: true,
 	})
@@ -402,7 +403,6 @@ func EditUserPost(ctx *context.Context) {
 			ctx.Data["Err_Password"] = true
 			ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplUserEdit, &form)
 		case password.IsErrIsPwnedRequest(err):
-			log.Error("%s", err.Error())
 			ctx.Data["Err_Password"] = true
 			ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
 		default:
@@ -412,7 +412,7 @@ func EditUserPost(ctx *context.Context) {
 	}
 
 	if form.Email != "" {
-		if err := user_service.AddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil {
+		if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil {
 			switch {
 			case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
 				ctx.Data["Err_Email"] = true
@@ -425,6 +425,9 @@ func EditUserPost(ctx *context.Context) {
 			}
 			return
 		}
+		if !user_model.IsEmailDomainAllowed(form.Email) {
+			ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", form.Email))
+		}
 	}
 
 	opts := &user_service.UpdateOptions{
@@ -439,6 +442,7 @@ func EditUserPost(ctx *context.Context) {
 		AllowCreateOrganization: optional.Some(form.AllowCreateOrganization),
 		IsRestricted:            optional.Some(form.Restricted),
 		Visibility:              optional.Some(form.Visibility),
+		Language:                optional.Some(form.Language),
 	}
 
 	if err := user_service.UpdateUser(ctx, u, opts); err != nil {
diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go
index 560ee70ea0..f6f9237858 100644
--- a/routers/web/admin/users_test.go
+++ b/routers/web/admin/users_test.go
@@ -8,10 +8,10 @@ import (
 
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/stretchr/testify/assert"
diff --git a/routers/web/auth/2fa.go b/routers/web/auth/2fa.go
index dc0062ebaa..f93177bf96 100644
--- a/routers/web/auth/2fa.go
+++ b/routers/web/auth/2fa.go
@@ -10,9 +10,9 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 )
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 3de1f3373d..8b5cd986b8 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -7,6 +7,7 @@ package auth
 import (
 	"errors"
 	"fmt"
+	"html/template"
 	"net/http"
 	"strings"
 
@@ -15,8 +16,8 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/eventsource"
+	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/session"
@@ -25,9 +26,9 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
-	"code.gitea.io/gitea/routers/utils"
 	auth_service "code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
@@ -37,12 +38,10 @@ import (
 )
 
 const (
-	// tplSignIn template for sign in page
-	tplSignIn base.TplName = "user/auth/signin"
-	// tplSignUp template path for sign up page
-	tplSignUp base.TplName = "user/auth/signup"
-	// TplActivate template path for activate user
-	TplActivate base.TplName = "user/auth/activate"
+	tplSignIn         base.TplName = "user/auth/signin"          // for sign in page
+	tplSignUp         base.TplName = "user/auth/signup"          // for sign up page
+	TplActivate       base.TplName = "user/auth/activate"        // for activate user
+	TplActivatePrompt base.TplName = "user/auth/activate_prompt" // for showing a message for user activation
 )
 
 // autoSignIn reads cookie and try to auto-login.
@@ -124,9 +123,21 @@ func resetLocale(ctx *context.Context, u *user_model.User) error {
 	return nil
 }
 
+func RedirectAfterLogin(ctx *context.Context) {
+	redirectTo := ctx.FormString("redirect_to")
+	if redirectTo == "" {
+		redirectTo = ctx.GetSiteCookie("redirect_to")
+	}
+	middleware.DeleteRedirectToCookie(ctx.Resp)
+	nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL)
+	if setting.LandingPageURL == setting.LandingPageLogin {
+		nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page
+	}
+	ctx.RedirectToCurrentSite(redirectTo, nextRedirectTo)
+}
+
 func CheckAutoLogin(ctx *context.Context) bool {
-	// Check auto-login
-	isSucceed, err := autoSignIn(ctx)
+	isSucceed, err := autoSignIn(ctx) // try to auto-login
 	if err != nil {
 		if errors.Is(err, auth_service.ErrAuthTokenInvalidHash) {
 			ctx.Flash.Error(ctx.Tr("auth.remember_me.compromised"), true)
@@ -139,17 +150,10 @@ func CheckAutoLogin(ctx *context.Context) bool {
 	redirectTo := ctx.FormString("redirect_to")
 	if len(redirectTo) > 0 {
 		middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
-	} else {
-		redirectTo = ctx.GetSiteCookie("redirect_to")
 	}
 
 	if isSucceed {
-		middleware.DeleteRedirectToCookie(ctx.Resp)
-		nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL)
-		if setting.LandingPageURL == setting.LandingPageLogin {
-			nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page
-		}
-		ctx.RedirectToFirst(redirectTo, nextRedirectTo)
+		RedirectAfterLogin(ctx)
 		return true
 	}
 
@@ -164,7 +168,12 @@ func SignIn(ctx *context.Context) {
 		return
 	}
 
-	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+	if ctx.IsSigned {
+		RedirectAfterLogin(ctx)
+		return
+	}
+
+	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
 		return
@@ -187,7 +196,7 @@ func SignIn(ctx *context.Context) {
 func SignInPost(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("sign_in")
 
-	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
 		return
@@ -359,10 +368,10 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
 		return setting.AppSubURL + "/"
 	}
 
-	if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
+	if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" && httplib.IsCurrentGiteaSiteURL(redirectTo) {
 		middleware.DeleteRedirectToCookie(ctx.Resp)
 		if obeyRedirect {
-			ctx.RedirectToFirst(redirectTo)
+			ctx.RedirectToCurrentSite(redirectTo)
 		}
 		return redirectTo
 	}
@@ -411,7 +420,7 @@ func SignUp(ctx *context.Context) {
 
 	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
 
-	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
 	if err != nil {
 		ctx.ServerError("UserSignUp", err)
 		return
@@ -440,7 +449,7 @@ func SignUpPost(ctx *context.Context) {
 
 	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
 
-	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+	oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
 	if err != nil {
 		ctx.ServerError("UserSignUp", err)
 		return
@@ -613,72 +622,87 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
 		}
 	}
 
-	// Send confirmation email
-	if !u.IsActive && u.ID > 1 {
-		if setting.Service.RegisterManualConfirm {
-			ctx.Data["ManualActivationOnly"] = true
-			ctx.HTML(http.StatusOK, TplActivate)
-			return false
-		}
+	// for active user or the first (admin) user, we don't need to send confirmation email
+	if u.IsActive || u.ID == 1 {
+		return true
+	}
 
-		mailer.SendActivateAccountMail(ctx.Locale, u)
-
-		ctx.Data["IsSendRegisterMail"] = true
-		ctx.Data["Email"] = u.Email
-		ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
-		ctx.HTML(http.StatusOK, TplActivate)
-
-		if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
-			log.Error("Set cache(MailResendLimit) fail: %v", err)
-		}
+	if setting.Service.RegisterManualConfirm {
+		renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.manual_activation_only"))
 		return false
 	}
 
-	return true
+	sendActivateEmail(ctx, u)
+	return false
+}
+
+func renderActivationPromptMessage(ctx *context.Context, msg template.HTML) {
+	ctx.Data["ActivationPromptMessage"] = msg
+	ctx.HTML(http.StatusOK, TplActivatePrompt)
+}
+
+func sendActivateEmail(ctx *context.Context, u *user_model.User) {
+	if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
+		renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.resent_limit_prompt"))
+		return
+	}
+
+	if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+		log.Error("Set cache(MailResendLimit) fail: %v", err)
+		renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.resent_limit_prompt"))
+		return
+	}
+
+	mailer.SendActivateAccountMail(ctx.Locale, u)
+
+	activeCodeLives := timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
+	msgHTML := ctx.Locale.Tr("auth.confirmation_mail_sent_prompt_ex", u.Email, activeCodeLives)
+	renderActivationPromptMessage(ctx, msgHTML)
+}
+
+func renderActivationVerifyPassword(ctx *context.Context, code string) {
+	ctx.Data["ActivationCode"] = code
+	ctx.Data["NeedVerifyLocalPassword"] = true
+	ctx.HTML(http.StatusOK, TplActivate)
+}
+
+func renderActivationChangeEmail(ctx *context.Context) {
+	ctx.HTML(http.StatusOK, TplActivate)
 }
 
 // Activate render activate user page
 func Activate(ctx *context.Context) {
 	code := ctx.FormString("code")
 
-	if len(code) == 0 {
-		ctx.Data["IsActivatePage"] = true
-		if ctx.Doer == nil || ctx.Doer.IsActive {
-			ctx.NotFound("invalid user", nil)
+	if code == "" {
+		if ctx.Doer == nil {
+			ctx.Redirect(setting.AppSubURL + "/user/login")
+			return
+		} else if ctx.Doer.IsActive {
+			ctx.Redirect(setting.AppSubURL + "/")
 			return
 		}
-		// Resend confirmation email.
-		if setting.Service.RegisterEmailConfirm {
-			if ctx.Cache.IsExist("MailResendLimit_" + ctx.Doer.LowerName) {
-				ctx.Data["ResendLimited"] = true
-			} else {
-				ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
-				mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
 
-				if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
-					log.Error("Set cache(MailResendLimit) fail: %v", err)
-				}
-			}
-		} else {
-			ctx.Data["ServiceNotEnabled"] = true
+		if setting.MailService == nil || !setting.Service.RegisterEmailConfirm {
+			renderActivationPromptMessage(ctx, ctx.Tr("auth.disable_register_mail"))
+			return
 		}
-		ctx.HTML(http.StatusOK, TplActivate)
+
+		// Resend confirmation email. FIXME: ideally this should be in a POST request
+		sendActivateEmail(ctx, ctx.Doer)
 		return
 	}
 
+	// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
 	user := user_model.VerifyUserActiveCode(ctx, code)
-	// if code is wrong
-	if user == nil {
-		ctx.Data["IsCodeInvalid"] = true
-		ctx.HTML(http.StatusOK, TplActivate)
+	if user == nil { // if code is wrong
+		renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
 		return
 	}
 
 	// if account is local account, verify password
 	if user.LoginSource == 0 {
-		ctx.Data["Code"] = code
-		ctx.Data["NeedsPassword"] = true
-		ctx.HTML(http.StatusOK, TplActivate)
+		renderActivationVerifyPassword(ctx, code)
 		return
 	}
 
@@ -688,31 +712,49 @@ func Activate(ctx *context.Context) {
 // ActivatePost handles account activation with password check
 func ActivatePost(ctx *context.Context) {
 	code := ctx.FormString("code")
-	if len(code) == 0 {
+	if ctx.Doer != nil && ctx.Doer.IsActive {
+		ctx.Redirect(setting.AppSubURL + "/user/activate") // it will redirect again to the correct page
+		return
+	}
+
+	if code == "" {
+		newEmail := strings.TrimSpace(ctx.FormString("change_email"))
+		if ctx.Doer != nil && newEmail != "" && !strings.EqualFold(ctx.Doer.Email, newEmail) {
+			if user_model.ValidateEmail(newEmail) != nil {
+				ctx.Flash.Error(ctx.Locale.Tr("form.email_invalid"), true)
+				renderActivationChangeEmail(ctx)
+				return
+			}
+			err := user_model.ChangeInactivePrimaryEmail(ctx, ctx.Doer.ID, ctx.Doer.Email, newEmail)
+			if err != nil {
+				ctx.Flash.Error(ctx.Locale.Tr("admin.emails.not_updated", newEmail), true)
+				renderActivationChangeEmail(ctx)
+				return
+			}
+			ctx.Doer.Email = newEmail
+		}
+		// FIXME: at the moment, GET request handles the "send confirmation email" action. But the old code does this redirect and then send a confirmation email.
 		ctx.Redirect(setting.AppSubURL + "/user/activate")
 		return
 	}
 
+	// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
 	user := user_model.VerifyUserActiveCode(ctx, code)
-	// if code is wrong
-	if user == nil {
-		ctx.Data["IsCodeInvalid"] = true
-		ctx.HTML(http.StatusOK, TplActivate)
+	if user == nil { // if code is wrong
+		renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
 		return
 	}
 
 	// if account is local account, verify password
 	if user.LoginSource == 0 {
 		password := ctx.FormString("password")
-		if len(password) == 0 {
-			ctx.Data["Code"] = code
-			ctx.Data["NeedsPassword"] = true
-			ctx.HTML(http.StatusOK, TplActivate)
+		if password == "" {
+			renderActivationVerifyPassword(ctx, code)
 			return
 		}
 		if !user.ValidatePassword(password) {
-			ctx.Data["IsPasswordInvalid"] = true
-			ctx.HTML(http.StatusOK, TplActivate)
+			ctx.Flash.Error(ctx.Locale.Tr("auth.invalid_password"), true)
+			renderActivationVerifyPassword(ctx, code)
 			return
 		}
 	}
@@ -766,7 +808,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
 	ctx.Flash.Success(ctx.Tr("auth.account_activated"))
 	if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
 		middleware.DeleteRedirectToCookie(ctx.Resp)
-		ctx.RedirectToFirst(redirectTo)
+		ctx.RedirectToCurrentSite(redirectTo)
 		return
 	}
 
diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go
new file mode 100644
index 0000000000..c6afbf877c
--- /dev/null
+++ b/routers/web/auth/auth_test.go
@@ -0,0 +1,43 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package auth
+
+import (
+	"net/http"
+	"net/url"
+	"testing"
+
+	"code.gitea.io/gitea/modules/test"
+	"code.gitea.io/gitea/services/contexttest"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestUserLogin(t *testing.T) {
+	ctx, resp := contexttest.MockContext(t, "/user/login")
+	SignIn(ctx)
+	assert.Equal(t, http.StatusOK, resp.Code)
+
+	ctx, resp = contexttest.MockContext(t, "/user/login")
+	ctx.IsSigned = true
+	SignIn(ctx)
+	assert.Equal(t, http.StatusSeeOther, resp.Code)
+	assert.Equal(t, "/", test.RedirectURL(resp))
+
+	ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to=/other")
+	ctx.IsSigned = true
+	SignIn(ctx)
+	assert.Equal(t, "/other", test.RedirectURL(resp))
+
+	ctx, resp = contexttest.MockContext(t, "/user/login")
+	ctx.Req.AddCookie(&http.Cookie{Name: "redirect_to", Value: "/other-cookie"})
+	ctx.IsSigned = true
+	SignIn(ctx)
+	assert.Equal(t, "/other-cookie", test.RedirectURL(resp))
+
+	ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to="+url.QueryEscape("https://example.com"))
+	ctx.IsSigned = true
+	SignIn(ctx)
+	assert.Equal(t, "/", test.RedirectURL(resp))
+}
diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index 1d94e52fe3..f744a57a43 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -12,13 +12,13 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	auth_service "code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 07140b6674..3189d1372e 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -9,6 +9,7 @@ import (
 	"errors"
 	"fmt"
 	"html"
+	"html/template"
 	"io"
 	"net/http"
 	"net/url"
@@ -21,7 +22,6 @@ import (
 	auth_module "code.gitea.io/gitea/modules/auth"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
@@ -33,6 +33,7 @@ import (
 	auth_service "code.gitea.io/gitea/services/auth"
 	source_service "code.gitea.io/gitea/services/auth/source"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 	user_service "code.gitea.io/gitea/services/user"
@@ -499,11 +500,11 @@ func AuthorizeOAuth(ctx *context.Context) {
 	ctx.Data["Scope"] = form.Scope
 	ctx.Data["Nonce"] = form.Nonce
 	if user != nil {
-		ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`<a href="%s">@%s</a>`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name))
+		ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`<a href="%s">@%s</a>`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name)))
 	} else {
-		ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName))
+		ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName)))
 	}
-	ctx.Data["ApplicationRedirectDomainHTML"] = "<strong>" + html.EscapeString(form.RedirectURI) + "</strong>"
+	ctx.Data["ApplicationRedirectDomainHTML"] = template.HTML("<strong>" + html.EscapeString(form.RedirectURI) + "</strong>")
 	// TODO document SESSION <=> FORM
 	err = ctx.Session.Set("client_id", app.ClientID)
 	if err != nil {
@@ -579,16 +580,8 @@ func GrantApplicationOAuth(ctx *context.Context) {
 
 // OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
 func OIDCWellKnown(ctx *context.Context) {
-	t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown", nil)
-	if err != nil {
-		ctx.ServerError("unable to find template", err)
-		return
-	}
-	ctx.Resp.Header().Set("Content-Type", "application/json")
 	ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
-	if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
-		ctx.ServerError("unable to execute template", err)
-	}
+	ctx.JSONTemplate("user/auth/oidc_wellknown")
 }
 
 // OIDCKeys generates the JSON Web Key Set
@@ -986,7 +979,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 			}
 
 			overwriteDefault := &user_model.CreateUserOverwriteOptions{
-				IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm && !setting.Service.RegisterManualConfirm),
+				IsActive: optional.Some(!setting.OAuth2Client.RegisterEmailConfirm && !setting.Service.RegisterManualConfirm),
 			}
 
 			source := authSource.Cfg.(*oauth2.Source)
@@ -1164,7 +1157,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
 
 		if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
 			middleware.DeleteRedirectToCookie(ctx.Resp)
-			ctx.RedirectToFirst(redirectTo)
+			ctx.RedirectToCurrentSite(redirectTo)
 			return
 		}
 
diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go
index 29ef772b1c..2143b8096a 100644
--- a/routers/web/auth/openid.go
+++ b/routers/web/auth/openid.go
@@ -11,12 +11,12 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/openid"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index 5af1696a64..0e88fe68f9 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -12,14 +12,13 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
-	"code.gitea.io/gitea/routers/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
 	user_service "code.gitea.io/gitea/services/user"
@@ -37,7 +36,7 @@ func ForgotPasswd(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
 
 	if setting.MailService == nil {
-		log.Warn(ctx.Tr("auth.disable_forgot_password_mail_admin"))
+		log.Warn("no mail service configured")
 		ctx.Data["IsResetDisable"] = true
 		ctx.HTML(http.StatusOK, tplForgotPassword)
 		return
@@ -204,7 +203,7 @@ func ResetPasswdPost(ctx *context.Context) {
 		Password:           optional.Some(ctx.FormString("password")),
 		MustChangePassword: optional.Some(false),
 	}
-	if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
+	if err := user_service.UpdateAuth(ctx, u, opts); err != nil {
 		ctx.Data["IsResetForm"] = true
 		ctx.Data["Err_Password"] = true
 		switch {
@@ -215,7 +214,6 @@ func ResetPasswdPost(ctx *context.Context) {
 		case errors.Is(err, password.ErrIsPwned):
 			ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplResetPassword, nil)
 		case password.IsErrIsPwnedRequest(err):
-			log.Error("%s", err.Error())
 			ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil)
 		default:
 			ctx.ServerError("UpdateAuth", err)
@@ -299,7 +297,6 @@ func MustChangePasswordPost(ctx *context.Context) {
 			ctx.Data["Err_Password"] = true
 			ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplMustChangePassword, &form)
 		case password.IsErrIsPwnedRequest(err):
-			log.Error("%s", err.Error())
 			ctx.Data["Err_Password"] = true
 			ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form)
 		default:
@@ -312,9 +309,9 @@ func MustChangePasswordPost(ctx *context.Context) {
 
 	log.Trace("User updated password: %s", ctx.Doer.Name)
 
-	if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
+	if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" {
 		middleware.DeleteRedirectToCookie(ctx.Resp)
-		ctx.RedirectToFirst(redirectTo)
+		ctx.RedirectToCurrentSite(redirectTo)
 		return
 	}
 
diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go
index 95c8d262a5..1079f44a08 100644
--- a/routers/web/auth/webauthn.go
+++ b/routers/web/auth/webauthn.go
@@ -11,9 +11,9 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	wa "code.gitea.io/gitea/modules/auth/webauthn"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/externalaccount"
 
 	"github.com/go-webauthn/webauthn/protocol"
diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
index 525ca9be53..dd20663f94 100644
--- a/routers/web/devtest/devtest.go
+++ b/routers/web/devtest/devtest.go
@@ -10,8 +10,8 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/templates"
+	"code.gitea.io/gitea/services/context"
 )
 
 // List all devtest templates, they will be used for e2e tests for the UI components
diff --git a/routers/web/events/events.go b/routers/web/events/events.go
index 1a5a162c1a..52f20e07dc 100644
--- a/routers/web/events/events.go
+++ b/routers/web/events/events.go
@@ -7,11 +7,11 @@ import (
 	"net/http"
 	"time"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/eventsource"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/routers/web/auth"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Events listens for events
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index d81884ec62..ecd7c33e01 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -6,11 +6,12 @@ package explore
 import (
 	"net/http"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -34,12 +35,11 @@ func Code(ctx *context.Context) {
 	language := ctx.FormTrim("l")
 	keyword := ctx.FormTrim("q")
 
-	queryType := ctx.FormTrim("t")
-	isMatch := queryType == "match"
+	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
 
 	ctx.Data["Keyword"] = keyword
 	ctx.Data["Language"] = language
-	ctx.Data["queryType"] = queryType
+	ctx.Data["IsFuzzy"] = isFuzzy
 	ctx.Data["PageIsViewCode"] = true
 
 	if keyword == "" {
@@ -77,7 +77,16 @@ func Code(ctx *context.Context) {
 	)
 
 	if (len(repoIDs) > 0) || isAdmin {
-		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
+			RepoIDs:        repoIDs,
+			Keyword:        keyword,
+			IsKeywordFuzzy: isFuzzy,
+			Language:       language,
+			Paginator: &db.ListOptions{
+				Page:     page,
+				PageSize: setting.UI.RepoSearchPagingNum,
+			},
+		})
 		if err != nil {
 			if code_indexer.IsAvailable(ctx) {
 				ctx.ServerError("SearchResults", err)
@@ -128,7 +137,7 @@ func Code(ctx *context.Context) {
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "l", "Language")
+	pager.AddParamString("l", language)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplExploreCode)
diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go
index dc1318beef..f8fd6ec38e 100644
--- a/routers/web/explore/org.go
+++ b/routers/web/explore/org.go
@@ -6,9 +6,10 @@ package explore
 import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Organizations render explore organizations page
@@ -24,8 +25,16 @@ func Organizations(ctx *context.Context) {
 		visibleTypes = append(visibleTypes, structs.VisibleTypeLimited, structs.VisibleTypePrivate)
 	}
 
-	if ctx.FormString("sort") == "" {
-		ctx.SetFormString("sort", setting.UI.ExploreDefaultSort)
+	supportedSortOrders := container.SetOf(
+		"newest",
+		"oldest",
+		"alphabetically",
+		"reversealphabetically",
+	)
+	sortOrder := ctx.FormString("sort")
+	if sortOrder == "" {
+		sortOrder = "newest"
+		ctx.SetFormString("sort", sortOrder)
 	}
 
 	RenderUserSearch(ctx, &user_model.SearchUserOptions{
@@ -33,5 +42,7 @@ func Organizations(ctx *context.Context) {
 		Type:        user_model.UserTypeOrganization,
 		ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
 		Visible:     visibleTypes,
+
+		SupportedSortOrders: supportedSortOrders,
 	}, tplExploreUsers)
 }
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
index 0446edebe6..66477a255c 100644
--- a/routers/web/explore/repo.go
+++ b/routers/web/explore/repo.go
@@ -10,10 +10,10 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/sitemap"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -109,6 +109,21 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
 	language := ctx.FormTrim("language")
 	ctx.Data["Language"] = language
 
+	archived := ctx.FormOptionalBool("archived")
+	ctx.Data["IsArchived"] = archived
+
+	fork := ctx.FormOptionalBool("fork")
+	ctx.Data["IsFork"] = fork
+
+	mirror := ctx.FormOptionalBool("mirror")
+	ctx.Data["IsMirror"] = mirror
+
+	template := ctx.FormOptionalBool("template")
+	ctx.Data["IsTemplate"] = template
+
+	private := ctx.FormOptionalBool("private")
+	ctx.Data["IsPrivate"] = private
+
 	repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
 		ListOptions: db.ListOptions{
 			Page:     page,
@@ -125,6 +140,11 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
 		Language:           language,
 		IncludeDescription: setting.UI.SearchRepoDescription,
 		OnlyShowRelevant:   opts.OnlyShowRelevant,
+		Archived:           archived,
+		Fork:               fork,
+		Mirror:             mirror,
+		Template:           template,
+		IsPrivate:          private,
 	})
 	if err != nil {
 		ctx.ServerError("SearchRepository", err)
@@ -149,8 +169,8 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
 
 	pager := context.NewPagination(int(count), opts.PageSize, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "topic", "TopicOnly")
-	pager.AddParam(ctx, "language", "Language")
+	pager.AddParamString("topic", fmt.Sprint(topicOnly))
+	pager.AddParamString("language", language)
 	pager.AddParamString(relevantReposOnlyParam, fmt.Sprint(opts.OnlyShowRelevant))
 	ctx.Data["Page"] = pager
 
diff --git a/routers/web/explore/topic.go b/routers/web/explore/topic.go
index bb1be310de..b4507ba28d 100644
--- a/routers/web/explore/topic.go
+++ b/routers/web/explore/topic.go
@@ -8,8 +8,8 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
@@ -23,7 +23,7 @@ func TopicSearch(ctx *context.Context) {
 		},
 	}
 
-	topics, total, err := repo_model.FindTopics(ctx, opts)
+	topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError)
 		return
diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go
index 09d31f95ef..b79a79fb2c 100644
--- a/routers/web/explore/user.go
+++ b/routers/web/explore/user.go
@@ -10,12 +10,13 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/sitemap"
 	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -79,10 +80,16 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions,
 		fallthrough
 	default:
 		// in case the sortType is not valid, we set it to recentupdate
+		sortOrder = "recentupdate"
 		ctx.Data["SortType"] = "recentupdate"
 		orderBy = "`user`.updated_unix DESC"
 	}
 
+	if opts.SupportedSortOrders != nil && !opts.SupportedSortOrders.Contains(sortOrder) {
+		ctx.NotFound("unsupported sort order", nil)
+		return
+	}
+
 	opts.Keyword = ctx.FormTrim("q")
 	opts.OrderBy = orderBy
 	if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
@@ -132,15 +139,25 @@ func Users(ctx *context.Context) {
 	ctx.Data["PageIsExploreUsers"] = true
 	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 
-	if ctx.FormString("sort") == "" {
-		ctx.SetFormString("sort", setting.UI.ExploreDefaultSort)
+	supportedSortOrders := container.SetOf(
+		"newest",
+		"oldest",
+		"alphabetically",
+		"reversealphabetically",
+	)
+	sortOrder := ctx.FormString("sort")
+	if sortOrder == "" {
+		sortOrder = "newest"
+		ctx.SetFormString("sort", sortOrder)
 	}
 
 	RenderUserSearch(ctx, &user_model.SearchUserOptions{
 		Actor:       ctx.Doer,
 		Type:        user_model.UserTypeIndividual,
 		ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
-		IsActive:    util.OptionalBoolTrue,
+		IsActive:    optional.Some(true),
 		Visible:     []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
+
+		SupportedSortOrders: supportedSortOrders,
 	}, tplExploreUsers)
 }
diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go
index f13038ff9b..80ce2ad198 100644
--- a/routers/web/feed/branch.go
+++ b/routers/web/feed/branch.go
@@ -9,7 +9,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/gorilla/feeds"
 )
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index 66b01d3680..3defa436a7 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -6,6 +6,7 @@ package feed
 import (
 	"fmt"
 	"html"
+	"html/template"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -13,12 +14,12 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/gorilla/feeds"
 )
@@ -49,7 +50,7 @@ func toReleaseLink(ctx *context.Context, act *activities_model.Action) string {
 
 // renderMarkdown creates a minimal markdown render context from an action.
 // If rendering fails, the original markdown text is returned
-func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) string {
+func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) template.HTML {
 	markdownCtx := &markup.RenderContext{
 		Ctx: ctx,
 		Links: markup.Links{
@@ -63,7 +64,7 @@ func renderMarkdown(ctx *context.Context, act *activities_model.Action, content
 	}
 	markdown, err := markdown.RenderString(markdownCtx, content)
 	if err != nil {
-		return content
+		return templates.SanitizeHTML(content) // old code did so: use SanitizeHTML to render in tmpl
 	}
 	return markdown
 }
@@ -73,125 +74,130 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 	for _, act := range actions {
 		act.LoadActUser(ctx)
 
-		var content, desc, title string
+		// TODO: the code seems quite strange (maybe not right)
+		// sometimes it uses text content but sometimes it uses HTML content
+		// it should clearly defines which kind of content it should use for the feed items: plan text or rich HTML
+		var title, desc string
+		var content template.HTML
 
 		link := &feeds.Link{Href: act.GetCommentHTMLURL(ctx)}
 
 		// title
 		title = act.ActUser.DisplayName() + " "
+		var titleExtra template.HTML
 		switch act.OpType {
 		case activities_model.ActionCreateRepo:
-			title += ctx.TrHTMLEscapeArgs("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
 			link.Href = act.GetRepoAbsoluteLink(ctx)
 		case activities_model.ActionRenameRepo:
-			title += ctx.TrHTMLEscapeArgs("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
 			link.Href = act.GetRepoAbsoluteLink(ctx)
 		case activities_model.ActionCommitRepo:
 			link.Href = toBranchLink(ctx, act)
 			if len(act.Content) != 0 {
-				title += ctx.TrHTMLEscapeArgs("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
+				titleExtra = ctx.Locale.Tr("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
 			} else {
-				title += ctx.TrHTMLEscapeArgs("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
+				titleExtra = ctx.Locale.Tr("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
 			}
 		case activities_model.ActionCreateIssue:
 			link.Href = toIssueLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionCreatePullRequest:
 			link.Href = toPullLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionTransferRepo:
 			link.Href = act.GetRepoAbsoluteLink(ctx)
-			title += ctx.TrHTMLEscapeArgs("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
 		case activities_model.ActionPushTag:
 			link.Href = toTagLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx))
 		case activities_model.ActionCommentIssue:
 			issueLink := toIssueLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = issueLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionMergePullRequest:
 			pullLink := toPullLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = pullLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionAutoMergePullRequest:
 			pullLink := toPullLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = pullLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionCloseIssue:
 			issueLink := toIssueLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = issueLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionReopenIssue:
 			issueLink := toIssueLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = issueLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionClosePullRequest:
 			pullLink := toPullLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = pullLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionReopenPullRequest:
 			pullLink := toPullLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = pullLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionDeleteTag:
 			link.Href = act.GetRepoAbsoluteLink(ctx)
-			title += ctx.TrHTMLEscapeArgs("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx))
 		case activities_model.ActionDeleteBranch:
 			link.Href = act.GetRepoAbsoluteLink(ctx)
-			title += ctx.TrHTMLEscapeArgs("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx))
 		case activities_model.ActionMirrorSyncPush:
 			srcLink := toSrcLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = srcLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
 		case activities_model.ActionMirrorSyncCreate:
 			srcLink := toSrcLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = srcLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
 		case activities_model.ActionMirrorSyncDelete:
 			link.Href = act.GetRepoAbsoluteLink(ctx)
-			title += ctx.TrHTMLEscapeArgs("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
 		case activities_model.ActionApprovePullRequest:
 			pullLink := toPullLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionRejectPullRequest:
 			pullLink := toPullLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionCommentPull:
 			pullLink := toPullLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
 		case activities_model.ActionPublishRelease:
 			releaseLink := toReleaseLink(ctx, act)
 			if link.Href == "#" {
 				link.Href = releaseLink
 			}
-			title += ctx.TrHTMLEscapeArgs("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content)
+			titleExtra = ctx.Locale.Tr("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content)
 		case activities_model.ActionPullReviewDismissed:
 			pullLink := toPullLink(ctx, act)
-			title += ctx.TrHTMLEscapeArgs("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1])
+			titleExtra = ctx.Locale.Tr("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1])
 		case activities_model.ActionStarRepo:
 			link.Href = act.GetRepoAbsoluteLink(ctx)
-			title += ctx.TrHTMLEscapeArgs("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
 		case activities_model.ActionWatchRepo:
 			link.Href = act.GetRepoAbsoluteLink(ctx)
-			title += ctx.TrHTMLEscapeArgs("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
+			titleExtra = ctx.Locale.Tr("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
 		default:
 			return nil, fmt.Errorf("unknown action type: %v", act.OpType)
 		}
@@ -226,22 +232,22 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 				desc = act.GetIssueTitle(ctx)
 				comment := act.GetIssueInfos()[1]
 				if len(comment) != 0 {
-					desc += "\n\n" + renderMarkdown(ctx, act, comment)
+					desc += "\n\n" + string(renderMarkdown(ctx, act, comment))
 				}
 			case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
 				desc = act.GetIssueInfos()[1]
 			case activities_model.ActionCloseIssue, activities_model.ActionReopenIssue, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest:
 				desc = act.GetIssueTitle(ctx)
 			case activities_model.ActionPullReviewDismissed:
-				desc = ctx.Tr("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2]
+				desc = ctx.Locale.TrString("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2]
 			}
 		}
 		if len(content) == 0 {
-			content = desc
+			content = templates.SanitizeHTML(desc)
 		}
 
 		items = append(items, &feeds.Item{
-			Title:       title,
+			Title:       template.HTMLEscapeString(title) + string(titleExtra),
 			Link:        link,
 			Description: desc,
 			IsPermaLink: "false",
@@ -251,7 +257,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 			},
 			Id:      fmt.Sprintf("%v: %v", strconv.FormatInt(act.ID, 10), link.Href),
 			Created: act.CreatedUnix.AsTime(),
-			Content: content,
+			Content: string(content),
 		})
 	}
 	return items, err
@@ -280,7 +286,8 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i
 			return nil, err
 		}
 
-		var title, content string
+		var title string
+		var content template.HTML
 
 		if rel.IsTag {
 			title = rel.TagName
@@ -309,7 +316,7 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i
 				Email: rel.Publisher.GetEmail(),
 			},
 			Id:      fmt.Sprintf("%v: %v", strconv.FormatInt(rel.ID, 10), link.Href),
-			Content: content,
+			Content: string(content),
 		})
 	}
 
diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go
index 56a9c54ddc..1ab768ff27 100644
--- a/routers/web/feed/file.go
+++ b/routers/web/feed/file.go
@@ -9,9 +9,9 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/gorilla/feeds"
 )
diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go
index 04f84c0c8d..08cbcd9e12 100644
--- a/routers/web/feed/profile.go
+++ b/routers/web/feed/profile.go
@@ -7,9 +7,9 @@ import (
 	"time"
 
 	activities_model "code.gitea.io/gitea/models/activities"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/gorilla/feeds"
 )
@@ -56,9 +56,9 @@ func showUserFeed(ctx *context.Context, formatType string) {
 	}
 
 	feed := &feeds.Feed{
-		Title:       ctx.Tr("home.feed_of", ctx.ContextUser.DisplayName()),
+		Title:       ctx.Locale.TrString("home.feed_of", ctx.ContextUser.DisplayName()),
 		Link:        &feeds.Link{Href: ctx.ContextUser.HTMLURL()},
-		Description: ctxUserDescription,
+		Description: string(ctxUserDescription),
 		Created:     time.Now(),
 	}
 
diff --git a/routers/web/feed/release.go b/routers/web/feed/release.go
index 57b0c92766..273f47e3b4 100644
--- a/routers/web/feed/release.go
+++ b/routers/web/feed/release.go
@@ -8,7 +8,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/gorilla/feeds"
 )
@@ -28,10 +28,10 @@ func ShowReleaseFeed(ctx *context.Context, repo *repo_model.Repository, isReleas
 	var link *feeds.Link
 
 	if isReleasesOnly {
-		title = ctx.Tr("repo.release.releases_for", repo.FullName())
+		title = ctx.Locale.TrString("repo.release.releases_for", repo.FullName())
 		link = &feeds.Link{Href: repo.HTMLURL() + "/release"}
 	} else {
-		title = ctx.Tr("repo.release.tags_for", repo.FullName())
+		title = ctx.Locale.TrString("repo.release.tags_for", repo.FullName())
 		link = &feeds.Link{Href: repo.HTMLURL() + "/tags"}
 	}
 
diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go
index 8931dae8cc..a41808c24a 100644
--- a/routers/web/feed/render.go
+++ b/routers/web/feed/render.go
@@ -4,7 +4,7 @@
 package feed
 
 import (
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 // RenderBranchFeed render format for branch or file
diff --git a/routers/web/feed/repo.go b/routers/web/feed/repo.go
index 5fcad26779..bfcc3a37d6 100644
--- a/routers/web/feed/repo.go
+++ b/routers/web/feed/repo.go
@@ -8,7 +8,7 @@ import (
 
 	activities_model "code.gitea.io/gitea/models/activities"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/gorilla/feeds"
 )
@@ -27,7 +27,7 @@ func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType
 	}
 
 	feed := &feeds.Feed{
-		Title:       ctx.Tr("home.feed_of", repo.FullName()),
+		Title:       ctx.Locale.TrString("home.feed_of", repo.FullName()),
 		Link:        &feeds.Link{Href: repo.HTMLURL()},
 		Description: repo.Description,
 		Created:     time.Now(),
diff --git a/routers/web/githttp.go b/routers/web/githttp.go
index ab74e9a333..5f1dedce76 100644
--- a/routers/web/githttp.go
+++ b/routers/web/githttp.go
@@ -6,11 +6,10 @@ package web
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/web/repo"
-	context_service "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 func requireSignIn(ctx *context.Context) {
@@ -39,5 +38,5 @@ func gitHTTPRouters(m *web.Route) {
 		m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
 		m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
 		m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
-	}, ignSignInAndCsrf, requireSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context_service.UserAssignmentWeb())
+	}, ignSignInAndCsrf, requireSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
 }
diff --git a/routers/web/goget.go b/routers/web/goget.go
index c5b8b6cbc0..8d5612ebfe 100644
--- a/routers/web/goget.go
+++ b/routers/web/goget.go
@@ -12,9 +12,9 @@ import (
 	"strings"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 func goGet(ctx *context.Context) {
diff --git a/routers/web/home.go b/routers/web/home.go
index 2321b00efe..d4be0931e8 100644
--- a/routers/web/home.go
+++ b/routers/web/home.go
@@ -12,15 +12,15 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/sitemap"
 	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/routers/web/auth"
 	"code.gitea.io/gitea/routers/web/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -71,7 +71,7 @@ func HomeSitemap(ctx *context.Context) {
 		_, cnt, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
 			Type:        user_model.UserTypeIndividual,
 			ListOptions: db.ListOptions{PageSize: 1},
-			IsActive:    util.OptionalBoolTrue,
+			IsActive:    optional.Some(true),
 			Visible:     []structs.VisibleType{structs.VisibleTypePublic},
 		})
 		if err != nil {
diff --git a/routers/web/misc/markup.go b/routers/web/misc/markup.go
index c91da9a7f1..2dbbd6fc09 100644
--- a/routers/web/misc/markup.go
+++ b/routers/web/misc/markup.go
@@ -5,10 +5,10 @@
 package misc
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/common"
+	"code.gitea.io/gitea/services/context"
 )
 
 // Markup render markup document to HTML
diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go
index 54c93763f6..ac5496ce91 100644
--- a/routers/web/misc/misc.go
+++ b/routers/web/misc/misc.go
@@ -15,7 +15,7 @@ import (
 )
 
 func SSHInfo(rw http.ResponseWriter, req *http.Request) {
-	if !git.SupportProcReceive {
+	if !git.DefaultFeatures.SupportProcReceive {
 		rw.WriteHeader(http.StatusNotFound)
 		return
 	}
diff --git a/routers/web/misc/swagger.go b/routers/web/misc/swagger.go
index 72c09a3780..5fddfa8885 100644
--- a/routers/web/misc/swagger.go
+++ b/routers/web/misc/swagger.go
@@ -7,7 +7,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 // tplSwagger swagger page template
diff --git a/routers/web/nodeinfo.go b/routers/web/nodeinfo.go
index 01b71e7086..f1cc7bf530 100644
--- a/routers/web/nodeinfo.go
+++ b/routers/web/nodeinfo.go
@@ -7,8 +7,8 @@ import (
 	"fmt"
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 type nodeInfoLinks struct {
diff --git a/routers/web/org/block.go b/routers/web/org/block.go
new file mode 100644
index 0000000000..d40458e250
--- /dev/null
+++ b/routers/web/org/block.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/modules/base"
+	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
+)
+
+const (
+	tplSettingsBlockedUsers base.TplName = "org/settings/blocked_users"
+)
+
+func BlockedUsers(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("user.block.list")
+	ctx.Data["PageIsOrgSettings"] = true
+	ctx.Data["PageIsSettingsBlockedUsers"] = true
+
+	shared_user.BlockedUsers(ctx, ctx.ContextUser)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
+}
+
+func BlockedUsersPost(ctx *context.Context) {
+	shared_user.BlockedUsersPost(ctx, ctx.ContextUser)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.Redirect(ctx.ContextUser.OrganisationLink() + "/settings/blocked_users")
+}
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 8bf02b2c42..846b1de18a 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -11,9 +11,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	repo_model "code.gitea.io/gitea/models/repo"
-	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
@@ -21,6 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -46,17 +45,6 @@ func Home(ctx *context.Context) {
 
 	ctx.Data["PageIsUserProfile"] = true
 	ctx.Data["Title"] = org.DisplayName()
-	if len(org.Description) != 0 {
-		desc, err := markdown.RenderString(&markup.RenderContext{
-			Ctx:   ctx,
-			Metas: map[string]string{"mode": "document"},
-		}, org.Description)
-		if err != nil {
-			ctx.ServerError("RenderString", err)
-			return
-		}
-		ctx.Data["RenderedDescription"] = desc
-	}
 
 	var orderBy db.SearchOrderBy
 	ctx.Data["SortType"] = ctx.FormString("sort")
@@ -97,6 +85,21 @@ func Home(ctx *context.Context) {
 		page = 1
 	}
 
+	archived := ctx.FormOptionalBool("archived")
+	ctx.Data["IsArchived"] = archived
+
+	fork := ctx.FormOptionalBool("fork")
+	ctx.Data["IsFork"] = fork
+
+	mirror := ctx.FormOptionalBool("mirror")
+	ctx.Data["IsMirror"] = mirror
+
+	template := ctx.FormOptionalBool("template")
+	ctx.Data["IsTemplate"] = template
+
+	private := ctx.FormOptionalBool("private")
+	ctx.Data["IsPrivate"] = private
+
 	var (
 		repos []*repo_model.Repository
 		count int64
@@ -114,6 +117,11 @@ func Home(ctx *context.Context) {
 		Actor:              ctx.Doer,
 		Language:           language,
 		IncludeDescription: setting.UI.SearchRepoDescription,
+		Archived:           archived,
+		Fork:               fork,
+		Mirror:             mirror,
+		Template:           template,
+		IsPrivate:          private,
 	})
 	if err != nil {
 		ctx.ServerError("SearchRepository", err)
@@ -131,18 +139,12 @@ func Home(ctx *context.Context) {
 		return
 	}
 
-	var isFollowing bool
-	if ctx.Doer != nil {
-		isFollowing = user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
-	}
-
 	ctx.Data["Repos"] = repos
 	ctx.Data["Total"] = count
 	ctx.Data["Members"] = members
 	ctx.Data["Teams"] = ctx.Org.Teams
 	ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
 	ctx.Data["PageIsViewRepositories"] = true
-	ctx.Data["IsFollowing"] = isFollowing
 
 	err = shared_user.LoadHeaderCount(ctx)
 	if err != nil {
@@ -152,7 +154,7 @@ func Home(ctx *context.Context) {
 
 	pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "language", "Language")
+	pager.AddParamString("language", language)
 	ctx.Data["Page"] = pager
 
 	ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
diff --git a/routers/web/org/members.go b/routers/web/org/members.go
index 15a615c706..63ac57cf0d 100644
--- a/routers/web/org/members.go
+++ b/routers/web/org/members.go
@@ -9,11 +9,12 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/organization"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -78,40 +79,43 @@ func Members(ctx *context.Context) {
 
 // MembersAction response for operation to a member of organization
 func MembersAction(ctx *context.Context) {
-	uid := ctx.FormInt64("uid")
-	if uid == 0 {
+	member, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
+	if err != nil {
+		log.Error("GetUserByID: %v", err)
+	}
+	if member == nil {
 		ctx.Redirect(ctx.Org.OrgLink + "/members")
 		return
 	}
 
 	org := ctx.Org.Organization
-	var err error
+
 	switch ctx.Params(":action") {
 	case "private":
-		if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
+		if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
 			ctx.Error(http.StatusNotFound)
 			return
 		}
-		err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, false)
+		err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, false)
 	case "public":
-		if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
+		if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
 			ctx.Error(http.StatusNotFound)
 			return
 		}
-		err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, true)
+		err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, true)
 	case "remove":
 		if !ctx.Org.IsOwner {
 			ctx.Error(http.StatusNotFound)
 			return
 		}
-		err = models.RemoveOrgUser(ctx, org.ID, uid)
+		err = models.RemoveOrgUser(ctx, org, member)
 		if organization.IsErrLastOrgOwner(err) {
 			ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
 			ctx.JSONRedirect(ctx.Org.OrgLink + "/members")
 			return
 		}
 	case "leave":
-		err = models.RemoveOrgUser(ctx, org.ID, ctx.Doer.ID)
+		err = models.RemoveOrgUser(ctx, org, ctx.Doer)
 		if err == nil {
 			ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName()))
 			ctx.JSON(http.StatusOK, map[string]any{
diff --git a/routers/web/org/org.go b/routers/web/org/org.go
index 52f8df8a1c..f94dd16eae 100644
--- a/routers/web/org/org.go
+++ b/routers/web/org/org.go
@@ -12,10 +12,10 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
@@ -29,7 +29,7 @@ func Create(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("new_org")
 	ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode
 	if !ctx.Doer.CanCreateOrganization() {
-		ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
+		ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
 		return
 	}
 	ctx.HTML(http.StatusOK, tplCreateOrg)
@@ -41,7 +41,7 @@ func CreatePost(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("new_org")
 
 	if !ctx.Doer.CanCreateOrganization() {
-		ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
+		ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
 		return
 	}
 
diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go
index f78bd00274..02eae8052e 100644
--- a/routers/web/org/org_labels.go
+++ b/routers/web/org/org_labels.go
@@ -8,10 +8,10 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/label"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index 03798a712c..596a370d2e 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -17,12 +17,13 @@ import (
 	attachment_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/web"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
@@ -66,7 +67,7 @@ func Projects(ctx *context.Context) {
 			PageSize: setting.UI.IssuePagingNum,
 		},
 		OwnerID:  ctx.ContextUser.ID,
-		IsClosed: util.OptionalBoolOf(isShowClosed),
+		IsClosed: optional.Some(isShowClosed),
 		OrderBy:  project_model.GetSearchOrderByBySortType(sortType),
 		Type:     projectType,
 		Title:    keyword,
@@ -78,7 +79,7 @@ func Projects(ctx *context.Context) {
 
 	opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
 		OwnerID:  ctx.ContextUser.ID,
-		IsClosed: util.OptionalBoolOf(!isShowClosed),
+		IsClosed: optional.Some(!isShowClosed),
 		Type:     projectType,
 	})
 	if err != nil {
@@ -104,7 +105,7 @@ func Projects(ctx *context.Context) {
 	}
 
 	for _, project := range projects {
-		project.RenderedContent = project.Description
+		project.RenderedContent = templates.SanitizeHTML(project.Description) // FIXME: is it right? why not render?
 	}
 
 	err = shared_user.LoadHeaderCount(ctx)
@@ -119,7 +120,7 @@ func Projects(ctx *context.Context) {
 	}
 
 	pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages)
-	pager.AddParam(ctx, "state", "State")
+	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
 	ctx.Data["Page"] = pager
 
 	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
@@ -206,11 +207,7 @@ func ChangeProjectStatus(ctx *context.Context) {
 	id := ctx.ParamsInt64(":id")
 
 	if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", err)
-		} else {
-			ctx.ServerError("ChangeProjectStatusByRepoIDAndID", err)
-		}
+		ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 	ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects?state=" + url.QueryEscape(ctx.Params(":action")))
@@ -220,11 +217,7 @@ func ChangeProjectStatus(ctx *context.Context) {
 func DeleteProject(ctx *context.Context) {
 	p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 	if p.OwnerID != ctx.ContextUser.ID {
@@ -253,11 +246,7 @@ func RenderEditProject(ctx *context.Context) {
 
 	p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 	if p.OwnerID != ctx.ContextUser.ID {
@@ -302,11 +291,7 @@ func EditProjectPost(ctx *context.Context) {
 
 	p, err := project_model.GetProjectByID(ctx, projectID)
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 	if p.OwnerID != ctx.ContextUser.ID {
@@ -334,11 +319,7 @@ func EditProjectPost(ctx *context.Context) {
 func ViewProject(ctx *context.Context) {
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 	if project.OwnerID != ctx.ContextUser.ID {
@@ -352,10 +333,6 @@ func ViewProject(ctx *context.Context) {
 		return
 	}
 
-	if boards[0].ID == 0 {
-		boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
-	}
-
 	issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
 	if err != nil {
 		ctx.ServerError("LoadIssuesOfBoards", err)
@@ -377,17 +354,17 @@ func ViewProject(ctx *context.Context) {
 	linkedPrsMap := make(map[int64][]*issues_model.Issue)
 	for _, issuesList := range issuesMap {
 		for _, issue := range issuesList {
-			var referencedIds []int64
+			var referencedIDs []int64
 			for _, comment := range issue.Comments {
 				if comment.RefIssueID != 0 && comment.RefIsPull {
-					referencedIds = append(referencedIds, comment.RefIssueID)
+					referencedIDs = append(referencedIDs, comment.RefIssueID)
 				}
 			}
 
-			if len(referencedIds) > 0 {
+			if len(referencedIDs) > 0 {
 				if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
-					IssueIDs: referencedIds,
-					IsPull:   util.OptionalBoolTrue,
+					IssueIDs: referencedIDs,
+					IsPull:   optional.Some(true),
 				}); err == nil {
 					linkedPrsMap[issue.ID] = linkedPrs
 				}
@@ -395,7 +372,7 @@ func ViewProject(ctx *context.Context) {
 		}
 	}
 
-	project.RenderedContent = project.Description
+	project.RenderedContent = templates.SanitizeHTML(project.Description) // FIXME: is it right? why not render?
 	ctx.Data["LinkedPRs"] = linkedPrsMap
 	ctx.Data["PageIsViewProjects"] = true
 	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
@@ -492,11 +469,7 @@ func DeleteProjectBoard(ctx *context.Context) {
 
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 
@@ -533,11 +506,7 @@ func AddBoardToProjectPost(ctx *context.Context) {
 
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 
@@ -565,11 +534,7 @@ func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
 
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return nil, nil
 	}
 
@@ -635,21 +600,6 @@ func SetDefaultProjectBoard(ctx *context.Context) {
 	ctx.JSONOK()
 }
 
-// UnsetDefaultProjectBoard unset default board for uncategorized issues/pulls
-func UnsetDefaultProjectBoard(ctx *context.Context) {
-	project, _ := CheckProjectBoardChangePermissions(ctx)
-	if ctx.Written() {
-		return
-	}
-
-	if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil {
-		ctx.ServerError("SetDefaultBoard", err)
-		return
-	}
-
-	ctx.JSONOK()
-}
-
 // MoveIssues moves or keeps issues in a column and sorts them inside that column
 func MoveIssues(ctx *context.Context) {
 	if ctx.Doer == nil {
@@ -661,11 +611,7 @@ func MoveIssues(ctx *context.Context) {
 
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if project_model.IsErrProjectNotExist(err) {
-			ctx.NotFound("ProjectNotExist", nil)
-		} else {
-			ctx.ServerError("GetProjectByID", err)
-		}
+		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
 		return
 	}
 	if project.OwnerID != ctx.ContextUser.ID {
@@ -673,28 +619,15 @@ func MoveIssues(ctx *context.Context) {
 		return
 	}
 
-	var board *project_model.Board
+	board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+	if err != nil {
+		ctx.NotFoundOrServerError("GetProjectBoard", project_model.IsErrProjectBoardNotExist, err)
+		return
+	}
 
-	if ctx.ParamsInt64(":boardID") == 0 {
-		board = &project_model.Board{
-			ID:        0,
-			ProjectID: project.ID,
-			Title:     ctx.Tr("repo.projects.type.uncategorized"),
-		}
-	} else {
-		board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
-		if err != nil {
-			if project_model.IsErrProjectBoardNotExist(err) {
-				ctx.NotFound("ProjectBoardNotExist", nil)
-			} else {
-				ctx.ServerError("GetProjectBoard", err)
-			}
-			return
-		}
-		if board.ProjectID != project.ID {
-			ctx.NotFound("BoardNotInProject", nil)
-			return
-		}
+	if board.ProjectID != project.ID {
+		ctx.NotFound("BoardNotInProject", nil)
+		return
 	}
 
 	type movedIssuesForm struct {
@@ -717,11 +650,7 @@ func MoveIssues(ctx *context.Context) {
 	}
 	movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
 	if err != nil {
-		if issues_model.IsErrIssueNotExist(err) {
-			ctx.NotFound("IssueNotExisting", nil)
-		} else {
-			ctx.ServerError("GetIssueByID", err)
-		}
+		ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err)
 		return
 	}
 
diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go
index 8053ab4cf9..f4ccfe1c06 100644
--- a/routers/web/org/projects_test.go
+++ b/routers/web/org/projects_test.go
@@ -7,8 +7,8 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/routers/web/org"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index 47d0063f76..494ada4323 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -14,7 +14,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
@@ -22,6 +21,7 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
 	user_setting "code.gitea.io/gitea/routers/web/user/setting"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	org_service "code.gitea.io/gitea/services/org"
 	repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/web/org/setting/runners.go b/routers/web/org/setting/runners.go
index c3c771036a..fe05709237 100644
--- a/routers/web/org/setting/runners.go
+++ b/routers/web/org/setting/runners.go
@@ -4,7 +4,7 @@
 package setting
 
 import (
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/org/setting_oauth2.go b/routers/web/org/setting_oauth2.go
index ca4fe09f38..7f855795d3 100644
--- a/routers/web/org/setting_oauth2.go
+++ b/routers/web/org/setting_oauth2.go
@@ -10,10 +10,10 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
 	user_setting "code.gitea.io/gitea/routers/web/user/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go
index 796829d34e..af9836e42c 100644
--- a/routers/web/org/setting_packages.go
+++ b/routers/web/org/setting_packages.go
@@ -8,10 +8,10 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	shared "code.gitea.io/gitea/routers/web/shared/packages"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go
index 71fe99c97c..144d9b1b43 100644
--- a/routers/web/org/teams.go
+++ b/routers/web/org/teams.go
@@ -5,6 +5,7 @@
 package org
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -20,11 +21,11 @@ import (
 	unit_model "code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	org_service "code.gitea.io/gitea/services/org"
@@ -77,9 +78,9 @@ func TeamsAction(ctx *context.Context) {
 			ctx.Error(http.StatusNotFound)
 			return
 		}
-		err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
+		err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer)
 	case "leave":
-		err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
+		err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer)
 		if err != nil {
 			if org_model.IsErrLastOrgOwner(err) {
 				ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
@@ -100,13 +101,13 @@ func TeamsAction(ctx *context.Context) {
 			return
 		}
 
-		uid := ctx.FormInt64("uid")
-		if uid == 0 {
+		user, _ := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
+		if user == nil {
 			ctx.Redirect(ctx.Org.OrgLink + "/teams")
 			return
 		}
 
-		err = models.RemoveTeamMember(ctx, ctx.Org.Team, uid)
+		err = models.RemoveTeamMember(ctx, ctx.Org.Team, user)
 		if err != nil {
 			if org_model.IsErrLastOrgOwner(err) {
 				ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
@@ -161,7 +162,7 @@ func TeamsAction(ctx *context.Context) {
 		if ctx.Org.Team.IsMember(ctx, u.ID) {
 			ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
 		} else {
-			err = models.AddTeamMember(ctx, ctx.Org.Team, u.ID)
+			err = models.AddTeamMember(ctx, ctx.Org.Team, u)
 		}
 
 		page = "team"
@@ -189,6 +190,8 @@ func TeamsAction(ctx *context.Context) {
 	if err != nil {
 		if org_model.IsErrLastOrgOwner(err) {
 			ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Flash.Error(ctx.Tr("org.teams.members.blocked_user"))
 		} else {
 			log.Error("Action(%s): %v", ctx.Params(":action"), err)
 			ctx.JSON(http.StatusOK, map[string]any{
@@ -590,7 +593,7 @@ func TeamInvitePost(ctx *context.Context) {
 		return
 	}
 
-	if err := models.AddTeamMember(ctx, team, ctx.Doer.ID); err != nil {
+	if err := models.AddTeamMember(ctx, team, ctx.Doer); err != nil {
 		ctx.ServerError("AddTeamMember", err)
 		return
 	}
diff --git a/routers/web/passkey.go b/routers/web/passkey.go
new file mode 100644
index 0000000000..0d10a69dfe
--- /dev/null
+++ b/routers/web/passkey.go
@@ -0,0 +1,24 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package web
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
+)
+
+type passkeyEndpointsType struct {
+	Enroll string `json:"enroll"`
+	Manage string `json:"manage"`
+}
+
+func passkeyEndpoints(ctx *context.Context) {
+	url := setting.AppURL + "user/settings/security"
+	ctx.JSON(http.StatusOK, passkeyEndpointsType{
+		Enroll: url,
+		Manage: url,
+	})
+}
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index fe528a483b..6059ad1414 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -15,11 +15,11 @@ import (
 	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/repo"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 
 	"github.com/nektos/act/pkg/model"
@@ -61,24 +61,24 @@ func List(ctx *context.Context) {
 
 	var workflows []Workflow
 	if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
-		ctx.Error(http.StatusInternalServerError, err.Error())
+		ctx.ServerError("IsEmpty", err)
 		return
 	} else if !empty {
 		commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
 		if err != nil {
-			ctx.Error(http.StatusInternalServerError, err.Error())
+			ctx.ServerError("GetBranchCommit", err)
 			return
 		}
 		entries, err := actions.ListWorkflows(commit)
 		if err != nil {
-			ctx.Error(http.StatusInternalServerError, err.Error())
+			ctx.ServerError("ListWorkflows", err)
 			return
 		}
 
 		// Get all runner labels
 		runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
 			RepoID:        ctx.Repo.Repository.ID,
-			IsOnline:      util.OptionalBoolTrue,
+			IsOnline:      optional.Some(true),
 			WithAvailable: true,
 		})
 		if err != nil {
@@ -95,17 +95,22 @@ func List(ctx *context.Context) {
 			workflow := Workflow{Entry: *entry}
 			content, err := actions.GetContentFromEntry(entry)
 			if err != nil {
-				ctx.Error(http.StatusInternalServerError, err.Error())
+				ctx.ServerError("GetContentFromEntry", err)
 				return
 			}
 			wf, err := model.ReadWorkflow(bytes.NewReader(content))
 			if err != nil {
-				workflow.ErrMsg = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", err.Error())
+				workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
 				workflows = append(workflows, workflow)
 				continue
 			}
-			// Check whether have matching runner
+			// The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
+			hasJobWithoutNeeds := false
+			// Check whether have matching runner and a job without "needs"
 			for _, j := range wf.Jobs {
+				if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
+					hasJobWithoutNeeds = true
+				}
 				runsOnList := j.RunsOn()
 				for _, ro := range runsOnList {
 					if strings.Contains(ro, "${{") {
@@ -115,7 +120,7 @@ func List(ctx *context.Context) {
 						continue
 					}
 					if !allRunnerLabels.Contains(ro) {
-						workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_matching_online_runner_helper", ro)
+						workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
 						break
 					}
 				}
@@ -123,6 +128,9 @@ func List(ctx *context.Context) {
 					break
 				}
 			}
+			if !hasJobWithoutNeeds {
+				workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
+			}
 			workflows = append(workflows, workflow)
 		}
 	}
@@ -172,7 +180,7 @@ func List(ctx *context.Context) {
 
 	runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, err.Error())
+		ctx.ServerError("FindAndCount", err)
 		return
 	}
 
@@ -181,7 +189,7 @@ func List(ctx *context.Context) {
 	}
 
 	if err := actions_model.RunList(runs).LoadTriggerUser(ctx); err != nil {
-		ctx.Error(http.StatusInternalServerError, err.Error())
+		ctx.ServerError("LoadTriggerUser", err)
 		return
 	}
 
@@ -189,7 +197,7 @@ func List(ctx *context.Context) {
 
 	actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, err.Error())
+		ctx.ServerError("GetActors", err)
 		return
 	}
 	ctx.Data["Actors"] = repo.MakeSelfOnTop(ctx.Doer, actors)
diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go
new file mode 100644
index 0000000000..6fa951826c
--- /dev/null
+++ b/routers/web/repo/actions/badge.go
@@ -0,0 +1,56 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"path/filepath"
+	"strings"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/modules/badge"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
+)
+
+func GetWorkflowBadge(ctx *context.Context) {
+	workflowFile := ctx.Params("workflow_name")
+	branch := ctx.Req.URL.Query().Get("branch")
+	if branch == "" {
+		branch = ctx.Repo.Repository.DefaultBranch
+	}
+	branchRef := fmt.Sprintf("refs/heads/%s", branch)
+	event := ctx.Req.URL.Query().Get("event")
+
+	badge, err := getWorkflowBadge(ctx, workflowFile, branchRef, event)
+	if err != nil {
+		ctx.ServerError("GetWorkflowBadge", err)
+		return
+	}
+
+	ctx.Data["Badge"] = badge
+	ctx.RespHeader().Set("Content-Type", "image/svg+xml")
+	ctx.HTML(http.StatusOK, "shared/actions/runner_badge")
+}
+
+func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event string) (badge.Badge, error) {
+	extension := filepath.Ext(workflowFile)
+	workflowName := strings.TrimSuffix(workflowFile, extension)
+
+	run, err := actions_model.GetWorkflowLatestRun(ctx, ctx.Repo.Repository.ID, workflowFile, branchName, event)
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			return badge.GenerateBadge(workflowName, "no status", badge.DefaultColor), nil
+		}
+		return badge.Badge{}, err
+	}
+
+	color, ok := badge.StatusColorMap[run.Status]
+	if !ok {
+		return badge.GenerateBadge(workflowName, "unknown status", badge.DefaultColor), nil
+	}
+	return badge.GenerateBadge(workflowName, run.Status.String(), color), nil
+}
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 9cda30d23d..41989589be 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -12,6 +12,7 @@ import (
 	"io"
 	"net/http"
 	"net/url"
+	"strconv"
 	"strings"
 	"time"
 
@@ -21,12 +22,13 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/base"
-	context_module "code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	actions_service "code.gitea.io/gitea/services/actions"
+	context_module "code.gitea.io/gitea/services/context"
 
 	"xorm.io/builder"
 )
@@ -57,15 +59,16 @@ type ViewRequest struct {
 type ViewResponse struct {
 	State struct {
 		Run struct {
-			Link       string     `json:"link"`
-			Title      string     `json:"title"`
-			Status     string     `json:"status"`
-			CanCancel  bool       `json:"canCancel"`
-			CanApprove bool       `json:"canApprove"` // the run needs an approval and the doer has permission to approve
-			CanRerun   bool       `json:"canRerun"`
-			Done       bool       `json:"done"`
-			Jobs       []*ViewJob `json:"jobs"`
-			Commit     ViewCommit `json:"commit"`
+			Link              string     `json:"link"`
+			Title             string     `json:"title"`
+			Status            string     `json:"status"`
+			CanCancel         bool       `json:"canCancel"`
+			CanApprove        bool       `json:"canApprove"` // the run needs an approval and the doer has permission to approve
+			CanRerun          bool       `json:"canRerun"`
+			CanDeleteArtifact bool       `json:"canDeleteArtifact"`
+			Done              bool       `json:"done"`
+			Jobs              []*ViewJob `json:"jobs"`
+			Commit            ViewCommit `json:"commit"`
 		} `json:"run"`
 		CurrentJob struct {
 			Title  string         `json:"title"`
@@ -146,6 +149,7 @@ func ViewPost(ctx *context_module.Context) {
 	resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
 	resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
 	resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
+	resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
 	resp.State.Run.Done = run.Status.IsDone()
 	resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
 	resp.State.Run.Status = run.Status.String()
@@ -168,8 +172,8 @@ func ViewPost(ctx *context_module.Context) {
 		Link: run.RefLink(),
 	}
 	resp.State.Run.Commit = ViewCommit{
-		LocaleCommit:   ctx.Tr("actions.runs.commit"),
-		LocalePushedBy: ctx.Tr("actions.runs.pushed_by"),
+		LocaleCommit:   ctx.Locale.TrString("actions.runs.commit"),
+		LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"),
 		ShortSha:       base.ShortSha(run.CommitSHA),
 		Link:           fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
 		Pusher:         pusher,
@@ -194,7 +198,7 @@ func ViewPost(ctx *context_module.Context) {
 	resp.State.CurrentJob.Title = current.Name
 	resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
 	if run.NeedApproval {
-		resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc")
+		resp.State.CurrentJob.Detail = ctx.Locale.TrString("actions.need_approval_desc")
 	}
 	resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
 	resp.Logs.StepsLog = make([]*ViewStepLog, 0)          // marshal to '[]' instead fo 'null' in json
@@ -260,10 +264,14 @@ func ViewPost(ctx *context_module.Context) {
 }
 
 // Rerun will rerun jobs in the given run
-// jobIndex = 0 means rerun all jobs
+// If jobIndexStr is a blank string, it means rerun all jobs
 func Rerun(ctx *context_module.Context) {
 	runIndex := ctx.ParamsInt64("run")
-	jobIndex := ctx.ParamsInt64("job")
+	jobIndexStr := ctx.Params("job")
+	var jobIndex int64
+	if jobIndexStr != "" {
+		jobIndex, _ = strconv.ParseInt(jobIndexStr, 10, 64)
+	}
 
 	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
 	if err != nil {
@@ -295,12 +303,25 @@ func Rerun(ctx *context_module.Context) {
 		return
 	}
 
-	if jobIndex != 0 {
-		jobs = []*actions_model.ActionRunJob{job}
+	if jobIndexStr == "" { // rerun all jobs
+		for _, j := range jobs {
+			// if the job has needs, it should be set to "blocked" status to wait for other jobs
+			shouldBlock := len(j.Needs) > 0
+			if err := rerunJob(ctx, j, shouldBlock); err != nil {
+				ctx.Error(http.StatusInternalServerError, err.Error())
+				return
+			}
+		}
+		ctx.JSON(http.StatusOK, struct{}{})
+		return
 	}
 
-	for _, j := range jobs {
-		if err := rerunJob(ctx, j); err != nil {
+	rerunJobs := actions_service.GetAllRerunJobs(job, jobs)
+
+	for _, j := range rerunJobs {
+		// jobs other than the specified one should be set to "blocked" status
+		shouldBlock := j.JobID != job.JobID
+		if err := rerunJob(ctx, j, shouldBlock); err != nil {
 			ctx.Error(http.StatusInternalServerError, err.Error())
 			return
 		}
@@ -309,7 +330,7 @@ func Rerun(ctx *context_module.Context) {
 	ctx.JSON(http.StatusOK, struct{}{})
 }
 
-func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) error {
+func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
 	status := job.Status
 	if !status.IsDone() {
 		return nil
@@ -317,6 +338,9 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) erro
 
 	job.TaskID = 0
 	job.Status = actions_model.StatusWaiting
+	if shouldBlock {
+		job.Status = actions_model.StatusBlocked
+	}
 	job.Started = 0
 	job.Stopped = 0
 
@@ -535,6 +559,29 @@ func ArtifactsView(ctx *context_module.Context) {
 	ctx.JSON(http.StatusOK, artifactsResponse)
 }
 
+func ArtifactsDeleteView(ctx *context_module.Context) {
+	if !ctx.Repo.CanWrite(unit.TypeActions) {
+		ctx.Error(http.StatusForbidden, "no permission")
+		return
+	}
+
+	runIndex := ctx.ParamsInt64("run")
+	artifactName := ctx.Params("artifact_name")
+
+	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
+	if err != nil {
+		ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
+			return errors.Is(err, util.ErrNotExist)
+		}, err)
+		return
+	}
+	if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
+		ctx.Error(http.StatusInternalServerError, err.Error())
+		return
+	}
+	ctx.JSON(http.StatusOK, struct{}{})
+}
+
 func ArtifactsDownloadView(ctx *context_module.Context) {
 	runIndex := ctx.ParamsInt64("run")
 	artifactName := ctx.Params("artifact_name")
@@ -562,8 +609,38 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
 		return
 	}
 
+	// if artifacts status is not uploaded-confirmed, treat it as not found
+	for _, art := range artifacts {
+		if art.Status != int64(actions_model.ArtifactStatusUploadConfirmed) {
+			ctx.Error(http.StatusNotFound, "artifact not found")
+			return
+		}
+	}
+
 	ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
 
+	// Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend
+	// The v4 backend enshures ContentEncoding is set to "application/zip", which is not the case for the old backend
+	if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" {
+		art := artifacts[0]
+		if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
+			u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath)
+			if u != nil && err == nil {
+				ctx.Redirect(u.String())
+				return
+			}
+		}
+		f, err := storage.ActionsArtifacts.Open(art.StoragePath)
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, err.Error())
+			return
+		}
+		_, _ = io.Copy(ctx.Resp, f)
+		return
+	}
+
+	// Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend
+	// Those need to be zipped for download
 	writer := zip.NewWriter(ctx.Resp)
 	defer writer.Close()
 	for _, art := range artifacts {
diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go
index 3d030edaca..6f6641cc65 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -10,7 +10,7 @@ import (
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -22,6 +22,8 @@ func Activity(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.activity")
 	ctx.Data["PageIsActivity"] = true
 
+	ctx.Data["PageIsPulse"] = true
+
 	ctx.Data["Period"] = ctx.Params("period")
 
 	timeUntil := time.Now()
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index 8c322b45e5..f0c5622aec 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -9,15 +9,15 @@ import (
 
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/httpcache"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/common"
 	"code.gitea.io/gitea/services/attachment"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index d414779a14..1887e4d95d 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -13,13 +13,15 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/highlight"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
+	files_service "code.gitea.io/gitea/services/repository/files"
 )
 
 type blameRow struct {
@@ -86,9 +88,16 @@ func RefBlame(ctx *context.Context) {
 
 	ctx.Data["IsBlame"] = true
 
-	ctx.Data["FileSize"] = blob.Size()
+	fileSize := blob.Size()
+	ctx.Data["FileSize"] = fileSize
 	ctx.Data["FileName"] = blob.Name()
 
+	if fileSize >= setting.UI.MaxDisplayFileSize {
+		ctx.Data["IsFileTooLarge"] = true
+		ctx.HTML(http.StatusOK, tplRepoHome)
+		return
+	}
+
 	ctx.Data["NumLines"], err = blob.GetBlobLineCount()
 	ctx.Data["NumLinesSet"] = true
 
@@ -131,11 +140,8 @@ type blameResult struct {
 }
 
 func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) {
-	objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
-	if err != nil {
-		ctx.NotFound("CreateBlameReader", err)
-		return nil, err
-	}
+	objectFormat := ctx.Repo.GetObjectFormat()
+
 	blameReader, err := git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, bypassBlameIgnore)
 	if err != nil {
 		return nil, err
@@ -247,31 +253,11 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st
 func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
 	repoLink := ctx.Repo.RepoLink
 
-	language := ""
-
-	indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
-	if err == nil {
-		defer deleteTemporaryFile()
-
-		filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
-			CachedOnly: true,
-			Attributes: []string{"linguist-language", "gitlab-language"},
-			Filenames:  []string{ctx.Repo.TreePath},
-			IndexFile:  indexFilename,
-			WorkTree:   worktree,
-		})
-		if err != nil {
-			log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
-		}
-
-		language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
-		if language == "" || language == "unspecified" {
-			language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
-		}
-		if language == "unspecified" {
-			language = ""
-		}
+	language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
+	if err != nil {
+		log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
 	}
+
 	lines := make([]string, 0)
 	rows := make([]*blameRow, 0)
 	escapeStatus := &charset.EscapeStatus{}
@@ -300,9 +286,9 @@ func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames
 
 				var avatar string
 				if commit.User != nil {
-					avatar = string(avatarUtils.Avatar(commit.User, 18, "gt-mr-3"))
+					avatar = string(avatarUtils.Avatar(commit.User, 18))
 				} else {
-					avatar = string(avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "gt-mr-3"))
+					avatar = string(avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "tw-mr-2"))
 				}
 
 				br.Avatar = gotemplate.HTML(avatar)
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index a31d611fc0..f879a98786 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -16,15 +16,15 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/utils"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	release_service "code.gitea.io/gitea/services/release"
 	repo_service "code.gitea.io/gitea/services/repository"
@@ -54,7 +54,7 @@ func Branches(ctx *context.Context) {
 
 	kw := ctx.FormString("q")
 
-	defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, util.OptionalBoolNone, kw, page, pageSize)
+	defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, optional.None[bool](), kw, page, pageSize)
 	if err != nil {
 		ctx.ServerError("LoadBranches", err)
 		return
@@ -148,12 +148,7 @@ func RestoreBranchPost(ctx *context.Context) {
 		return
 	}
 
-	objectFormat, err := gitrepo.GetObjectFormatOfRepo(ctx, ctx.Repo.Repository)
-	if err != nil {
-		log.Error("RestoreBranch: CreateBranch: %w", err)
-		ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", deletedBranch.Name))
-		return
-	}
+	objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName)
 
 	// Don't return error below this
 	if err := repo_service.PushUpdate(
@@ -232,7 +227,7 @@ func CreateBranch(ctx *context.Context) {
 			if len(e.Message) == 0 {
 				ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message"))
 			} else {
-				flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+				flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 					"Message": ctx.Tr("repo.editor.push_rejected"),
 					"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
 					"Details": utils.SanitizeFlashErrorString(e.Message),
diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go
index 25dd881219..088f8d889d 100644
--- a/routers/web/repo/cherry_pick.go
+++ b/routers/web/repo/cherry_pick.go
@@ -12,11 +12,11 @@ import (
 	git_model "code.gitea.io/gitea/models/git"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/repository/files"
 )
@@ -104,9 +104,9 @@ func CherryPickPost(ctx *context.Context) {
 	message := strings.TrimSpace(form.CommitSummary)
 	if message == "" {
 		if form.Revert {
-			message = ctx.Tr("repo.commit.revert-header", sha)
+			message = ctx.Locale.TrString("repo.commit.revert-header", sha)
 		} else {
-			message = ctx.Tr("repo.commit.cherry-pick-header", sha)
+			message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha)
 		}
 	}
 
diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go
new file mode 100644
index 0000000000..c76f492da0
--- /dev/null
+++ b/routers/web/repo/code_frequency.go
@@ -0,0 +1,41 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+	"errors"
+	"net/http"
+
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/services/context"
+	contributors_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+	tplCodeFrequency base.TplName = "repo/activity"
+)
+
+// CodeFrequency renders the page to show repository code frequency
+func CodeFrequency(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.code_frequency")
+
+	ctx.Data["PageIsActivity"] = true
+	ctx.Data["PageIsCodeFrequency"] = true
+	ctx.PageData["repoLink"] = ctx.Repo.RepoLink
+
+	ctx.HTML(http.StatusOK, tplCodeFrequency)
+}
+
+// CodeFrequencyData returns JSON of code frequency data
+func CodeFrequencyData(ctx *context.Context) {
+	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+		if errors.Is(err, contributors_service.ErrAwaitGeneration) {
+			ctx.Status(http.StatusAccepted)
+			return
+		}
+		ctx.ServerError("GetCodeFrequencyData", err)
+	} else {
+		ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
+	}
+}
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 32fa973ef6..8543fa44cc 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -19,7 +19,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitgraph"
 	"code.gitea.io/gitea/modules/gitrepo"
@@ -27,6 +26,7 @@ import (
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/gitdiff"
 	git_service "code.gitea.io/gitea/services/repository"
 )
@@ -163,8 +163,8 @@ func Graph(ctx *context.Context) {
 	ctx.Data["CommitCount"] = commitsCount
 
 	paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
-	paginator.AddParam(ctx, "mode", "Mode")
-	paginator.AddParam(ctx, "hide-pr-refs", "HidePRRefs")
+	paginator.AddParamString("mode", mode)
+	paginator.AddParamString("hide-pr-refs", fmt.Sprint(hidePRRefs))
 	for _, branch := range branches {
 		paginator.AddParamString("branch", branch)
 	}
@@ -203,7 +203,7 @@ func SearchCommits(ctx *context.Context) {
 
 	ctx.Data["Keyword"] = query
 	if all {
-		ctx.Data["All"] = "checked"
+		ctx.Data["All"] = true
 	}
 	ctx.Data["Username"] = ctx.Repo.Owner.Name
 	ctx.Data["Reponame"] = ctx.Repo.Repository.Name
@@ -351,7 +351,7 @@ func Diff(ctx *context.Context) {
 	ctx.Data["Commit"] = commit
 	ctx.Data["Diff"] = diff
 
-	statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptions{ListAll: true})
+	statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
 	if err != nil {
 		log.Error("GetLatestCommitStatus: %v", err)
 	}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index a3593815b8..cfb0e859bd 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -25,17 +25,18 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/context"
 	csv_module "code.gitea.io/gitea/modules/csv"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/typesniffer"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/gitdiff"
 )
 
@@ -126,7 +127,7 @@ func setCsvCompareContext(ctx *context.Context) {
 			return CsvDiffResult{nil, ""}
 		}
 
-		errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large"))
+		errTooLarge := errors.New(ctx.Locale.TrString("repo.error.csv.too_large"))
 
 		csvReaderFromCommit := func(ctx *markup.RenderContext, blob *git.Blob) (*csv.Reader, io.Closer, error) {
 			if blob == nil {
@@ -311,14 +312,14 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
 	baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
 	baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(ci.BaseBranch)
 	baseIsTag := ctx.Repo.GitRepo.IsTagExist(ci.BaseBranch)
-	objectFormat, _ := ctx.Repo.GitRepo.GetObjectFormat()
+
 	if !baseIsCommit && !baseIsBranch && !baseIsTag {
 		// Check if baseBranch is short sha commit hash
 		if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil {
 			ci.BaseBranch = baseCommit.ID.String()
 			ctx.Data["BaseBranch"] = ci.BaseBranch
 			baseIsCommit = true
-		} else if ci.BaseBranch == objectFormat.EmptyObjectID().String() {
+		} else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
 			if isSameRepo {
 				ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch))
 			} else {
@@ -696,11 +697,9 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
 	defer gitRepo.Close()
 
 	branches, err = git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
-		RepoID: repo.ID,
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
-		IsDeletedBranch: util.OptionalBoolFalse,
+		RepoID:          repo.ID,
+		ListOptions:     db.ListOptionsAll,
+		IsDeletedBranch: optional.Some(false),
 	})
 	if err != nil {
 		return nil, nil, err
@@ -753,11 +752,9 @@ func CompareDiff(ctx *context.Context) {
 	}
 
 	headBranches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
-		RepoID: ci.HeadRepo.ID,
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
-		IsDeletedBranch: util.OptionalBoolFalse,
+		RepoID:          ci.HeadRepo.ID,
+		ListOptions:     db.ListOptionsAll,
+		IsDeletedBranch: optional.Some(false),
 	})
 	if err != nil {
 		ctx.ServerError("GetBranches", err)
@@ -979,5 +976,8 @@ func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chu
 		}
 		diffLines = append(diffLines, diffLine)
 	}
+	if err = scanner.Err(); err != nil {
+		return nil, fmt.Errorf("getExcerptLines scan: %w", err)
+	}
 	return diffLines, nil
 }
diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go
new file mode 100644
index 0000000000..5fda17469e
--- /dev/null
+++ b/routers/web/repo/contributors.go
@@ -0,0 +1,44 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+	"errors"
+	"net/http"
+
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/services/context"
+	contributors_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+	tplContributors base.TplName = "repo/activity"
+)
+
+// Contributors render the page to show repository contributors graph
+func Contributors(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.contributors")
+
+	ctx.Data["PageIsActivity"] = true
+	ctx.Data["PageIsContributors"] = true
+
+	ctx.PageData["contributionType"] = "commits"
+
+	ctx.PageData["repoLink"] = ctx.Repo.RepoLink
+
+	ctx.HTML(http.StatusOK, tplContributors)
+}
+
+// ContributorsData renders JSON of contributors along with their weekly commit statistics
+func ContributorsData(ctx *context.Context) {
+	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+		if errors.Is(err, contributors_service.ErrAwaitGeneration) {
+			ctx.Status(http.StatusAccepted)
+			return
+		}
+		ctx.ServerError("GetContributorStats", err)
+	} else {
+		ctx.JSON(http.StatusOK, contributorStats)
+	}
+}
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index a9e2e2b2fa..c4a8baecca 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -9,7 +9,6 @@ import (
 	"time"
 
 	git_model "code.gitea.io/gitea/models/git"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/httpcache"
 	"code.gitea.io/gitea/modules/lfs"
@@ -17,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/routers/common"
+	"code.gitea.io/gitea/services/context"
 )
 
 // ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 85d40e7820..474f7ff1da 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -16,17 +16,17 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/typesniffer"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/utils"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/forms"
 	files_service "code.gitea.io/gitea/services/repository/files"
 )
@@ -80,8 +80,12 @@ func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName,
 		}
 	}
 
-	// Redirect to viewing file or folder
-	ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(newBranchName) + "/" + util.PathEscapeSegments(treePath))
+	returnURI := ctx.FormString("return_uri")
+
+	ctx.RedirectToCurrentSite(
+		returnURI,
+		ctx.Repo.RepoLink+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath),
+	)
 }
 
 // getParentTreeFields returns list of parent tree names and corresponding tree paths
@@ -100,6 +104,7 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
 }
 
 func editFile(ctx *context.Context, isNewFile bool) {
+	ctx.Data["PageIsViewCode"] = true
 	ctx.Data["PageIsEdit"] = true
 	ctx.Data["IsNewFile"] = isNewFile
 	canCommit := renderCommitRights(ctx)
@@ -161,9 +166,6 @@ func editFile(ctx *context.Context, isNewFile bool) {
 		}
 
 		d, _ := io.ReadAll(dataRc)
-		if err := dataRc.Close(); err != nil {
-			log.Error("Error whilst closing blob data: %v", err)
-		}
 
 		buf = append(buf, d...)
 		if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
@@ -193,6 +195,9 @@ func editFile(ctx *context.Context, isNewFile bool) {
 	ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
 	ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
 
+	ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
+	ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
+
 	ctx.HTML(http.StatusOK, tplEditFile)
 }
 
@@ -262,9 +267,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
 	message := strings.TrimSpace(form.CommitSummary)
 	if len(message) == 0 {
 		if isNewFile {
-			message = ctx.Tr("repo.editor.add", form.TreePath)
+			message = ctx.Locale.TrString("repo.editor.add", form.TreePath)
 		} else {
-			message = ctx.Tr("repo.editor.update", form.TreePath)
+			message = ctx.Locale.TrString("repo.editor.update", form.TreePath)
 		}
 	}
 	form.CommitMessage = strings.TrimSpace(form.CommitMessage)
@@ -336,15 +341,15 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
 				ctx.Error(http.StatusInternalServerError, err.Error())
 			}
 		} else if models.IsErrCommitIDDoesNotMatch(err) {
-			ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplEditFile, &form)
+			ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching"), tplEditFile, &form)
 		} else if git.IsErrPushOutOfDate(err) {
-			ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplEditFile, &form)
+			ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date"), tplEditFile, &form)
 		} else if git.IsErrPushRejected(err) {
 			errPushRej := err.(*git.ErrPushRejected)
 			if len(errPushRej.Message) == 0 {
 				ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
 			} else {
-				flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+				flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 					"Message": ctx.Tr("repo.editor.push_rejected"),
 					"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
 					"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
@@ -356,7 +361,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
 				ctx.RenderWithErr(flashError, tplEditFile, &form)
 			}
 		} else {
-			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 				"Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
 				"Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
 				"Details": utils.SanitizeFlashErrorString(err.Error()),
@@ -415,7 +420,7 @@ func DiffPreviewPost(ctx *context.Context) {
 	}
 
 	if diff.NumFiles == 0 {
-		ctx.PlainText(http.StatusOK, ctx.Tr("repo.editor.no_changes_to_show"))
+		ctx.PlainText(http.StatusOK, ctx.Locale.TrString("repo.editor.no_changes_to_show"))
 		return
 	}
 	ctx.Data["File"] = diff.Files[0]
@@ -482,7 +487,7 @@ func DeleteFilePost(ctx *context.Context) {
 
 	message := strings.TrimSpace(form.CommitSummary)
 	if len(message) == 0 {
-		message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
+		message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath)
 	}
 	form.CommitMessage = strings.TrimSpace(form.CommitMessage)
 	if len(form.CommitMessage) > 0 {
@@ -545,7 +550,7 @@ func DeleteFilePost(ctx *context.Context) {
 			if len(errPushRej.Message) == 0 {
 				ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
 			} else {
-				flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+				flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 					"Message": ctx.Tr("repo.editor.push_rejected"),
 					"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
 					"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
@@ -691,7 +696,7 @@ func UploadFilePost(ctx *context.Context) {
 		if dir == "" {
 			dir = "/"
 		}
-		message = ctx.Tr("repo.editor.upload_files_to_dir", dir)
+		message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir)
 	}
 
 	form.CommitMessage = strings.TrimSpace(form.CommitMessage)
@@ -745,7 +750,7 @@ func UploadFilePost(ctx *context.Context) {
 			if len(errPushRej.Message) == 0 {
 				ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
 			} else {
-				flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+				flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 					"Message": ctx.Tr("repo.editor.push_rejected"),
 					"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
 					"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go
index c28c3ef1d6..313fcfe33a 100644
--- a/routers/web/repo/editor_test.go
+++ b/routers/web/repo/editor_test.go
@@ -7,9 +7,9 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/routers/web/repo/find.go b/routers/web/repo/find.go
index daefe59c8f..9da4237c1e 100644
--- a/routers/web/repo/find.go
+++ b/routers/web/repo/find.go
@@ -7,7 +7,8 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -17,7 +18,7 @@ const (
 // FindFiles render the page to find repository files
 func FindFiles(ctx *context.Context) {
 	path := ctx.Params("*")
-	ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + path
-	ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + path
+	ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + util.PathEscapeSegments(path)
+	ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + util.PathEscapeSegments(path)
 	ctx.HTML(http.StatusOK, tplFindFiles)
 }
diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go
new file mode 100644
index 0000000000..27e42a8f98
--- /dev/null
+++ b/routers/web/repo/fork.go
@@ -0,0 +1,236 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+	"errors"
+	"net/http"
+	"net/url"
+
+	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
+	"code.gitea.io/gitea/models/organization"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/forms"
+	repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+	tplFork base.TplName = "repo/pulls/fork"
+)
+
+func getForkRepository(ctx *context.Context) *repo_model.Repository {
+	forkRepo := ctx.Repo.Repository
+	if ctx.Written() {
+		return nil
+	}
+
+	if forkRepo.IsEmpty {
+		log.Trace("Empty repository %-v", forkRepo)
+		ctx.NotFound("getForkRepository", nil)
+		return nil
+	}
+
+	if err := forkRepo.LoadOwner(ctx); err != nil {
+		ctx.ServerError("LoadOwner", err)
+		return nil
+	}
+
+	ctx.Data["repo_name"] = forkRepo.Name
+	ctx.Data["description"] = forkRepo.Description
+	ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
+	canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID)
+
+	ctx.Data["ForkRepo"] = forkRepo
+
+	ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID)
+	if err != nil {
+		ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
+		return nil
+	}
+	var orgs []*organization.Organization
+	for _, org := range ownedOrgs {
+		if forkRepo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) {
+			orgs = append(orgs, org)
+		}
+	}
+
+	traverseParentRepo := forkRepo
+	for {
+		if ctx.Doer.ID == traverseParentRepo.OwnerID {
+			canForkToUser = false
+		} else {
+			for i, org := range orgs {
+				if org.ID == traverseParentRepo.OwnerID {
+					orgs = append(orgs[:i], orgs[i+1:]...)
+					break
+				}
+			}
+		}
+
+		if !traverseParentRepo.IsFork {
+			break
+		}
+		traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
+		if err != nil {
+			ctx.ServerError("GetRepositoryByID", err)
+			return nil
+		}
+	}
+
+	ctx.Data["CanForkToUser"] = canForkToUser
+	ctx.Data["Orgs"] = orgs
+
+	if canForkToUser {
+		ctx.Data["ContextUser"] = ctx.Doer
+	} else if len(orgs) > 0 {
+		ctx.Data["ContextUser"] = orgs[0]
+	} else {
+		ctx.Data["CanForkRepo"] = false
+		ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
+		return nil
+	}
+
+	branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
+		RepoID:          ctx.Repo.Repository.ID,
+		ListOptions:     db.ListOptionsAll,
+		IsDeletedBranch: optional.Some(false),
+		// Add it as the first option
+		ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch},
+	})
+	if err != nil {
+		ctx.ServerError("FindBranchNames", err)
+		return nil
+	}
+	ctx.Data["Branches"] = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
+
+	return forkRepo
+}
+
+// Fork render repository fork page
+func Fork(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("new_fork")
+
+	if ctx.Doer.CanForkRepo() {
+		ctx.Data["CanForkRepo"] = true
+	} else {
+		maxCreationLimit := ctx.Doer.MaxCreationLimit()
+		msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+		ctx.Flash.Error(msg, true)
+	}
+
+	getForkRepository(ctx)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.HTML(http.StatusOK, tplFork)
+}
+
+// ForkPost response for forking a repository
+func ForkPost(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.CreateRepoForm)
+	ctx.Data["Title"] = ctx.Tr("new_fork")
+	ctx.Data["CanForkRepo"] = true
+
+	ctxUser := checkContextUser(ctx, form.UID)
+	if ctx.Written() {
+		return
+	}
+
+	forkRepo := getForkRepository(ctx)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.Data["ContextUser"] = ctxUser
+
+	if ctx.HasError() {
+		ctx.HTML(http.StatusOK, tplFork)
+		return
+	}
+
+	var err error
+	traverseParentRepo := forkRepo
+	for {
+		if ctxUser.ID == traverseParentRepo.OwnerID {
+			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+			return
+		}
+		repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
+		if repo != nil {
+			ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+			return
+		}
+		if !traverseParentRepo.IsFork {
+			break
+		}
+		traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
+		if err != nil {
+			ctx.ServerError("GetRepositoryByID", err)
+			return
+		}
+	}
+
+	// Check if user is allowed to create repo's on the organization.
+	if ctxUser.IsOrganization() {
+		isAllowedToFork, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID)
+		if err != nil {
+			ctx.ServerError("CanCreateOrgRepo", err)
+			return
+		} else if !isAllowedToFork {
+			ctx.Error(http.StatusForbidden)
+			return
+		}
+	}
+
+	repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
+		BaseRepo:     forkRepo,
+		Name:         form.RepoName,
+		Description:  form.Description,
+		SingleBranch: form.ForkSingleBranch,
+	})
+	if err != nil {
+		ctx.Data["Err_RepoName"] = true
+		switch {
+		case repo_model.IsErrReachLimitOfRepo(err):
+			maxCreationLimit := ctxUser.MaxCreationLimit()
+			msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+			ctx.RenderWithErr(msg, tplFork, &form)
+		case repo_model.IsErrRepoAlreadyExist(err):
+			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+		case repo_model.IsErrRepoFilesAlreadyExist(err):
+			switch {
+			case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
+				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
+			case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
+				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
+			case setting.Repository.AllowDeleteOfUnadoptedRepositories:
+				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
+			default:
+				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
+			}
+		case db.IsErrNameReserved(err):
+			ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
+		case db.IsErrNamePatternNotAllowed(err):
+			ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
+		case errors.Is(err, user_model.ErrBlockedUser):
+			ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
+		default:
+			ctx.ServerError("ForkPost", err)
+		}
+		return
+	}
+
+	log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
+	ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+}
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index f52abbfb02..8fb6d93068 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -24,13 +24,13 @@ import (
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 
 	"github.com/go-chi/cors"
@@ -183,7 +183,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
 
 		if repoExist {
 			// Because of special ref "refs/for" .. , need delay write permission check
-			if git.SupportProcReceive {
+			if git.DefaultFeatures.SupportProcReceive {
 				accessMode = perm.AccessModeRead
 			}
 
diff --git a/routers/web/repo/helper.go b/routers/web/repo/helper.go
index a98abe566f..5e1e116018 100644
--- a/routers/web/repo/helper.go
+++ b/routers/web/repo/helper.go
@@ -8,8 +8,8 @@ import (
 	"sort"
 
 	"code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/services/context"
 )
 
 func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index c58cef8d64..6ff983f71a 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -9,6 +9,7 @@ import (
 	stdCtx "context"
 	"errors"
 	"fmt"
+	"html/template"
 	"math/big"
 	"net/http"
 	"net/url"
@@ -31,7 +32,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/emoji"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
@@ -39,21 +40,25 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/templates/vars"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/utils"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	issue_service "code.gitea.io/gitea/services/issue"
 	pull_service "code.gitea.io/gitea/services/pull"
 	repo_service "code.gitea.io/gitea/services/repository"
+	user_service "code.gitea.io/gitea/services/user"
 )
 
 const (
@@ -137,7 +142,7 @@ func MustAllowPulls(ctx *context.Context) {
 	}
 }
 
-func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
+func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) {
 	var err error
 	viewType := ctx.FormString("type")
 	sortType := ctx.FormString("sort")
@@ -183,8 +188,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 	if len(selectLabels) > 0 {
 		labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
 		if err != nil {
-			ctx.ServerError("StringsToInt64s", err)
-			return
+			ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
 		}
 	}
 
@@ -238,18 +242,18 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 		}
 	}
 
-	var isShowClosed util.OptionalBool
+	var isShowClosed optional.Option[bool]
 	switch ctx.FormString("state") {
 	case "closed":
-		isShowClosed = util.OptionalBoolTrue
+		isShowClosed = optional.Some(true)
 	case "all":
-		isShowClosed = util.OptionalBoolNone
+		isShowClosed = optional.None[bool]()
 	default:
-		isShowClosed = util.OptionalBoolFalse
+		isShowClosed = optional.Some(false)
 	}
 	// if there are closed issues and no open issues, default to showing all issues
 	if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
-		isShowClosed = util.OptionalBoolNone
+		isShowClosed = optional.None[bool]()
 	}
 
 	if repo.IsTimetrackerEnabled(ctx) {
@@ -269,10 +273,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 	}
 
 	var total int
-	switch isShowClosed {
-	case util.OptionalBoolTrue:
+	switch {
+	case isShowClosed.Value():
 		total = int(issueStats.ClosedCount)
-	case util.OptionalBoolNone:
+	case !isShowClosed.Has():
 		total = int(issueStats.OpenCount + issueStats.ClosedCount)
 	default:
 		total = int(issueStats.OpenCount)
@@ -320,15 +324,15 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 		return
 	}
 
-	// Get posters.
-	for i := range issues {
-		// Check read status
-		if !ctx.IsSigned {
-			issues[i].IsRead = true
-		} else if err = issues[i].GetIsRead(ctx, ctx.Doer.ID); err != nil {
-			ctx.ServerError("GetIsRead", err)
+	if ctx.IsSigned {
+		if err := issues.LoadIsRead(ctx, ctx.Doer.ID); err != nil {
+			ctx.ServerError("LoadIsRead", err)
 			return
 		}
+	} else {
+		for i := range issues {
+			issues[i].IsRead = true
+		}
 	}
 
 	commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
@@ -428,7 +432,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 		return
 	}
 
-	pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.IsTrue())
+	pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.Value())
 	if err != nil {
 		ctx.ServerError("GetPinnedIssues", err)
 		return
@@ -442,13 +446,13 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 	linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&archived=%t"
 	ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link,
 		url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels),
-		mentionedID, projectID, assigneeID, posterID, archived)
+		milestoneID, projectID, assigneeID, posterID, archived)
 	ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link,
 		url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels),
-		mentionedID, projectID, assigneeID, posterID, archived)
+		milestoneID, projectID, assigneeID, posterID, archived)
 	ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Link,
 		url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels),
-		mentionedID, projectID, assigneeID, posterID, archived)
+		milestoneID, projectID, assigneeID, posterID, archived)
 	ctx.Data["SelLabelIDs"] = labelIDs
 	ctx.Data["SelectLabels"] = selectLabels
 	ctx.Data["ViewType"] = viewType
@@ -458,26 +462,26 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 	ctx.Data["AssigneeID"] = assigneeID
 	ctx.Data["PosterID"] = posterID
 	ctx.Data["Keyword"] = keyword
-	switch isShowClosed {
-	case util.OptionalBoolTrue:
+	switch {
+	case isShowClosed.Value():
 		ctx.Data["State"] = "closed"
-	case util.OptionalBoolNone:
+	case !isShowClosed.Has():
 		ctx.Data["State"] = "all"
 	default:
 		ctx.Data["State"] = "open"
 	}
 	ctx.Data["ShowArchivedLabels"] = archived
 
-	pager.AddParam(ctx, "q", "Keyword")
-	pager.AddParam(ctx, "type", "ViewType")
-	pager.AddParam(ctx, "sort", "SortType")
-	pager.AddParam(ctx, "state", "State")
-	pager.AddParam(ctx, "labels", "SelectLabels")
-	pager.AddParam(ctx, "milestone", "MilestoneID")
-	pager.AddParam(ctx, "project", "ProjectID")
-	pager.AddParam(ctx, "assignee", "AssigneeID")
-	pager.AddParam(ctx, "poster", "PosterID")
-	pager.AddParam(ctx, "archived", "ShowArchivedLabels")
+	pager.AddParamString("q", keyword)
+	pager.AddParamString("type", viewType)
+	pager.AddParamString("sort", sortType)
+	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
+	pager.AddParamString("labels", fmt.Sprint(selectLabels))
+	pager.AddParamString("milestone", fmt.Sprint(milestoneID))
+	pager.AddParamString("project", fmt.Sprint(projectID))
+	pager.AddParamString("assignee", fmt.Sprint(assigneeID))
+	pager.AddParamString("poster", fmt.Sprint(posterID))
+	pager.AddParamString("archived", fmt.Sprint(archived))
 
 	ctx.Data["Page"] = pager
 }
@@ -510,7 +514,7 @@ func Issues(ctx *context.Context) {
 		ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
 	}
 
-	issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList))
+	issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList))
 	if ctx.Written() {
 		return
 	}
@@ -552,7 +556,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
 	var err error
 	ctx.Data["OpenMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
 		RepoID:   repo.ID,
-		IsClosed: util.OptionalBoolFalse,
+		IsClosed: optional.Some(false),
 	})
 	if err != nil {
 		ctx.ServerError("GetMilestones", err)
@@ -560,7 +564,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
 	}
 	ctx.Data["ClosedMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
 		RepoID:   repo.ID,
-		IsClosed: util.OptionalBoolTrue,
+		IsClosed: optional.Some(true),
 	})
 	if err != nil {
 		ctx.ServerError("GetMilestones", err)
@@ -584,52 +588,63 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
 	if repo.Owner.IsOrganization() {
 		repoOwnerType = project_model.TypeOrganization
 	}
+
+	projectsUnit := repo.MustGetUnit(ctx, unit.TypeProjects)
+
+	var openProjects []*project_model.Project
+	var closedProjects []*project_model.Project
 	var err error
-	projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
-		ListOptions: db.ListOptionsAll,
-		RepoID:      repo.ID,
-		IsClosed:    util.OptionalBoolFalse,
-		Type:        project_model.TypeRepository,
-	})
-	if err != nil {
-		ctx.ServerError("GetProjects", err)
-		return
-	}
-	projects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
-		ListOptions: db.ListOptionsAll,
-		OwnerID:     repo.OwnerID,
-		IsClosed:    util.OptionalBoolFalse,
-		Type:        repoOwnerType,
-	})
-	if err != nil {
-		ctx.ServerError("GetProjects", err)
-		return
+
+	if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
+		openProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
+			ListOptions: db.ListOptionsAll,
+			RepoID:      repo.ID,
+			IsClosed:    optional.Some(false),
+			Type:        project_model.TypeRepository,
+		})
+		if err != nil {
+			ctx.ServerError("GetProjects", err)
+			return
+		}
+		closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
+			ListOptions: db.ListOptionsAll,
+			RepoID:      repo.ID,
+			IsClosed:    optional.Some(true),
+			Type:        project_model.TypeRepository,
+		})
+		if err != nil {
+			ctx.ServerError("GetProjects", err)
+			return
+		}
 	}
 
-	ctx.Data["OpenProjects"] = append(projects, projects2...)
-
-	projects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
-		ListOptions: db.ListOptionsAll,
-		RepoID:      repo.ID,
-		IsClosed:    util.OptionalBoolTrue,
-		Type:        project_model.TypeRepository,
-	})
-	if err != nil {
-		ctx.ServerError("GetProjects", err)
-		return
-	}
-	projects2, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
-		ListOptions: db.ListOptionsAll,
-		OwnerID:     repo.OwnerID,
-		IsClosed:    util.OptionalBoolTrue,
-		Type:        repoOwnerType,
-	})
-	if err != nil {
-		ctx.ServerError("GetProjects", err)
-		return
+	if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeOwner) {
+		openProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
+			ListOptions: db.ListOptionsAll,
+			OwnerID:     repo.OwnerID,
+			IsClosed:    optional.Some(false),
+			Type:        repoOwnerType,
+		})
+		if err != nil {
+			ctx.ServerError("GetProjects", err)
+			return
+		}
+		openProjects = append(openProjects, openProjects2...)
+		closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
+			ListOptions: db.ListOptionsAll,
+			OwnerID:     repo.OwnerID,
+			IsClosed:    optional.Some(true),
+			Type:        repoOwnerType,
+		})
+		if err != nil {
+			ctx.ServerError("GetProjects", err)
+			return
+		}
+		closedProjects = append(closedProjects, closedProjects2...)
 	}
 
-	ctx.Data["ClosedProjects"] = append(projects, projects2...)
+	ctx.Data["OpenProjects"] = openProjects
+	ctx.Data["ClosedProjects"] = closedProjects
 }
 
 // repoReviewerSelection items to bee shown
@@ -712,16 +727,12 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
 			tmp.ItemID = -review.ReviewerTeamID
 		}
 
-		if ctx.Repo.IsAdmin() {
-			// Admin can dismiss or re-request any review requests
+		if canChooseReviewer {
+			// Users who can choose reviewers can also remove review requests
 			tmp.CanChange = true
 		} else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
 			// A user can refuse review requests
 			tmp.CanChange = true
-		} else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != issues_model.ReviewTypeRequest &&
-			ctx.Doer.ID != review.ReviewerID {
-			// The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
-			tmp.CanChange = true
 		}
 
 		pullReviews = append(pullReviews, tmp)
@@ -994,17 +1005,17 @@ func NewIssue(ctx *context.Context) {
 	}
 	ctx.Data["Tags"] = tags
 
-	_, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+	ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
 	templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
 	for k, v := range errs {
-		templateErrs[k] = v
+		ret.TemplateErrors[k] = v
 	}
 	if ctx.Written() {
 		return
 	}
 
-	if len(templateErrs) > 0 {
-		ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
+	if len(ret.TemplateErrors) > 0 {
+		ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true)
 	}
 
 	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
@@ -1018,7 +1029,7 @@ func NewIssue(ctx *context.Context) {
 	ctx.HTML(http.StatusOK, tplIssueNew)
 }
 
-func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string {
+func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) template.HTML {
 	var files []string
 	for k := range errs {
 		files = append(files, k)
@@ -1030,14 +1041,14 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string
 		lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file]))
 	}
 
-	flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+	flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 		"Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"),
 		"Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)),
 		"Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")),
 	})
 	if err != nil {
 		log.Debug("render flash error: %v", err)
-		flashError = ctx.Tr("repo.issues.choose.ignore_invalid_templates")
+		flashError = ctx.Locale.Tr("repo.issues.choose.ignore_invalid_templates")
 	}
 	return flashError
 }
@@ -1047,11 +1058,11 @@ func NewIssueChooseTemplate(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.issues.new")
 	ctx.Data["PageIsIssueList"] = true
 
-	issueTemplates, errs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
-	ctx.Data["IssueTemplates"] = issueTemplates
+	ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+	ctx.Data["IssueTemplates"] = ret.IssueTemplates
 
-	if len(errs) > 0 {
-		ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true)
+	if len(ret.TemplateErrors) > 0 {
+		ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true)
 	}
 
 	if !issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) {
@@ -1213,6 +1224,14 @@ func NewIssuePost(ctx *context.Context) {
 		return
 	}
 
+	if projectID > 0 {
+		if !ctx.Repo.CanRead(unit.TypeProjects) {
+			// User must also be able to see the project.
+			ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
+			return
+		}
+	}
+
 	if setting.Attachment.Enabled {
 		attachments = form.Files
 	}
@@ -1245,27 +1264,17 @@ func NewIssuePost(ctx *context.Context) {
 		Ref:         form.Ref,
 	}
 
-	if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
+	if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectID); err != nil {
 		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
-			return
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.JSONError(ctx.Tr("repo.issues.new.blocked_user"))
+		} else {
+			ctx.ServerError("NewIssue", err)
 		}
-		ctx.ServerError("NewIssue", err)
 		return
 	}
 
-	if projectID > 0 {
-		if !ctx.Repo.CanRead(unit.TypeProjects) {
-			// User must also be able to see the project.
-			ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
-			return
-		}
-		if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
-			ctx.ServerError("ChangeProjectAssign", err)
-			return
-		}
-	}
-
 	log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
 	if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 {
 		ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10))
@@ -1440,7 +1449,7 @@ func ViewIssue(ctx *context.Context) {
 		return
 	}
 
-	ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
+	ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title))
 
 	iw := new(issues_model.IssueWatch)
 	if ctx.Doer != nil {
@@ -1526,18 +1535,9 @@ func ViewIssue(ctx *context.Context) {
 	}
 
 	if issue.IsPull {
-		canChooseReviewer := ctx.Repo.CanWrite(unit.TypePullRequests)
+		canChooseReviewer := false
 		if ctx.Doer != nil && ctx.IsSigned {
-			if !canChooseReviewer {
-				canChooseReviewer = ctx.Doer.ID == issue.PosterID
-			}
-			if !canChooseReviewer {
-				canChooseReviewer, err = issues_model.IsOfficialReviewer(ctx, issue, ctx.Doer)
-				if err != nil {
-					ctx.ServerError("IsOfficialReviewer", err)
-					return
-				}
-			}
+			canChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, issue)
 		}
 
 		RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
@@ -1602,22 +1602,22 @@ func ViewIssue(ctx *context.Context) {
 	}
 	marked[issue.PosterID] = issue.ShowRole
 
-	// Render comments and and fetch participants.
+	// Render comments and fetch participants.
 	participants[0] = issue.Poster
+
+	if err := issue.Comments.LoadAttachmentsByIssue(ctx); err != nil {
+		ctx.ServerError("LoadAttachmentsByIssue", err)
+		return
+	}
+	if err := issue.Comments.LoadPosters(ctx); err != nil {
+		ctx.ServerError("LoadPosters", err)
+		return
+	}
+
 	for _, comment = range issue.Comments {
 		comment.Issue = issue
 
-		if err := comment.LoadPoster(ctx); err != nil {
-			ctx.ServerError("LoadPoster", err)
-			return
-		}
-
 		if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
-			if err := comment.LoadAttachments(ctx); err != nil {
-				ctx.ServerError("LoadAttachments", err)
-				return
-			}
-
 			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
 				Links: markup.Links{
 					Base: ctx.Repo.RepoLink,
@@ -1656,7 +1656,7 @@ func ViewIssue(ctx *context.Context) {
 			}
 			ghostMilestone := &issues_model.Milestone{
 				ID:   -1,
-				Name: ctx.Tr("repo.issues.deleted_milestone"),
+				Name: ctx.Locale.TrString("repo.issues.deleted_milestone"),
 			}
 			if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
 				comment.OldMilestone = ghostMilestone
@@ -1665,7 +1665,6 @@ func ViewIssue(ctx *context.Context) {
 				comment.Milestone = ghostMilestone
 			}
 		} else if comment.Type == issues_model.CommentTypeProject {
-
 			if err = comment.LoadProject(ctx); err != nil {
 				ctx.ServerError("LoadProject", err)
 				return
@@ -1673,7 +1672,7 @@ func ViewIssue(ctx *context.Context) {
 
 			ghostProject := &project_model.Project{
 				ID:    -1,
-				Title: ctx.Tr("repo.issues.deleted_project"),
+				Title: ctx.Locale.TrString("repo.issues.deleted_project"),
 			}
 
 			if comment.OldProjectID > 0 && comment.OldProject == nil {
@@ -1768,7 +1767,7 @@ func ViewIssue(ctx *context.Context) {
 				// so "|" is used as delimeter to mark the new format
 				if comment.Content[0] != '|' {
 					// handle old time comments that have formatted text stored
-					comment.RenderedContent = comment.Content
+					comment.RenderedContent = templates.SanitizeHTML(comment.Content)
 					comment.Content = ""
 				} else {
 					// else it's just a duration in seconds to pass on to the frontend
@@ -1863,6 +1862,8 @@ func ViewIssue(ctx *context.Context) {
 				mergeStyle = repo_model.MergeStyleRebaseMerge
 			} else if prConfig.AllowSquash {
 				mergeStyle = repo_model.MergeStyleSquash
+			} else if prConfig.AllowFastForwardOnly {
+				mergeStyle = repo_model.MergeStyleFastForwardOnly
 			} else if prConfig.AllowManualMerge {
 				mergeStyle = repo_model.MergeStyleManuallyMerged
 			}
@@ -2040,6 +2041,10 @@ func ViewIssue(ctx *context.Context) {
 	}
 	ctx.Data["Tags"] = tags
 
+	ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+		return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+	}
+
 	ctx.HTML(http.StatusOK, tplIssueView)
 }
 
@@ -2175,7 +2180,7 @@ func GetIssueInfo(ctx *context.Context) {
 		}
 	}
 
-	ctx.JSON(http.StatusOK, convert.ToIssue(ctx, issue))
+	ctx.JSON(http.StatusOK, convert.ToIssue(ctx, ctx.Doer, issue))
 }
 
 // UpdateIssueTitle change issue's title
@@ -2243,7 +2248,11 @@ func UpdateIssueContent(ctx *context.Context) {
 	}
 
 	if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
-		ctx.ServerError("ChangeContent", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user"))
+		} else {
+			ctx.ServerError("ChangeContent", err)
+		}
 		return
 	}
 
@@ -2490,6 +2499,10 @@ func UpdatePullReviewRequest(ctx *context.Context) {
 
 		_, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, reviewer, action == "attach")
 		if err != nil {
+			if issues_model.IsErrReviewRequestOnClosedPR(err) {
+				ctx.Status(http.StatusForbidden)
+				return
+			}
 			ctx.ServerError("ReviewRequest", err)
 			return
 		}
@@ -2506,14 +2519,14 @@ func SearchIssues(ctx *context.Context) {
 		return
 	}
 
-	var isClosed util.OptionalBool
+	var isClosed optional.Option[bool]
 	switch ctx.FormString("state") {
 	case "closed":
-		isClosed = util.OptionalBoolTrue
+		isClosed = optional.Some(true)
 	case "all":
-		isClosed = util.OptionalBoolNone
+		isClosed = optional.None[bool]()
 	default:
-		isClosed = util.OptionalBoolFalse
+		isClosed = optional.Some(false)
 	}
 
 	var (
@@ -2526,7 +2539,7 @@ func SearchIssues(ctx *context.Context) {
 			Private:     false,
 			AllPublic:   true,
 			TopicOnly:   false,
-			Collaborate: util.OptionalBoolNone,
+			Collaborate: optional.None[bool](),
 			// This needs to be a column that is not nil in fixtures or
 			// MySQL will return different results when sorting by null in some cases
 			OrderBy: db.SearchOrderByAlphabetically,
@@ -2549,7 +2562,7 @@ func SearchIssues(ctx *context.Context) {
 			opts.OwnerID = owner.ID
 			opts.AllLimited = false
 			opts.AllPublic = false
-			opts.Collaborate = util.OptionalBoolFalse
+			opts.Collaborate = optional.Some(false)
 		}
 		if ctx.FormString("team") != "" {
 			if ctx.FormString("owner") == "" {
@@ -2588,14 +2601,12 @@ func SearchIssues(ctx *context.Context) {
 		keyword = ""
 	}
 
-	var isPull util.OptionalBool
+	isPull := optional.None[bool]()
 	switch ctx.FormString("type") {
 	case "pulls":
-		isPull = util.OptionalBoolTrue
+		isPull = optional.Some(true)
 	case "issues":
-		isPull = util.OptionalBoolFalse
-	default:
-		isPull = util.OptionalBoolNone
+		isPull = optional.Some(false)
 	}
 
 	var includedAnyLabels []int64
@@ -2627,9 +2638,9 @@ func SearchIssues(ctx *context.Context) {
 		}
 	}
 
-	var projectID *int64
+	projectID := optional.None[int64]()
 	if v := ctx.FormInt64("project"); v > 0 {
-		projectID = &v
+		projectID = optional.Some(v)
 	}
 
 	// this api is also used in UI,
@@ -2658,28 +2669,28 @@ func SearchIssues(ctx *context.Context) {
 	}
 
 	if since != 0 {
-		searchOpt.UpdatedAfterUnix = &since
+		searchOpt.UpdatedAfterUnix = optional.Some(since)
 	}
 	if before != 0 {
-		searchOpt.UpdatedBeforeUnix = &before
+		searchOpt.UpdatedBeforeUnix = optional.Some(before)
 	}
 
 	if ctx.IsSigned {
 		ctxUserID := ctx.Doer.ID
 		if ctx.FormBool("created") {
-			searchOpt.PosterID = &ctxUserID
+			searchOpt.PosterID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("assigned") {
-			searchOpt.AssigneeID = &ctxUserID
+			searchOpt.AssigneeID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("mentioned") {
-			searchOpt.MentionID = &ctxUserID
+			searchOpt.MentionID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("review_requested") {
-			searchOpt.ReviewRequestedID = &ctxUserID
+			searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
 		}
 		if ctx.FormBool("reviewed") {
-			searchOpt.ReviewedID = &ctxUserID
+			searchOpt.ReviewedID = optional.Some(ctxUserID)
 		}
 	}
 
@@ -2699,7 +2710,7 @@ func SearchIssues(ctx *context.Context) {
 	}
 
 	ctx.SetTotalCountHeader(total)
-	ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues))
+	ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues))
 }
 
 func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
@@ -2730,14 +2741,14 @@ func ListIssues(ctx *context.Context) {
 		return
 	}
 
-	var isClosed util.OptionalBool
+	var isClosed optional.Option[bool]
 	switch ctx.FormString("state") {
 	case "closed":
-		isClosed = util.OptionalBoolTrue
+		isClosed = optional.Some(true)
 	case "all":
-		isClosed = util.OptionalBoolNone
+		isClosed = optional.None[bool]()
 	default:
-		isClosed = util.OptionalBoolFalse
+		isClosed = optional.Some(false)
 	}
 
 	keyword := ctx.FormTrim("q")
@@ -2784,19 +2795,17 @@ func ListIssues(ctx *context.Context) {
 		}
 	}
 
-	var projectID *int64
+	projectID := optional.None[int64]()
 	if v := ctx.FormInt64("project"); v > 0 {
-		projectID = &v
+		projectID = optional.Some(v)
 	}
 
-	var isPull util.OptionalBool
+	isPull := optional.None[bool]()
 	switch ctx.FormString("type") {
 	case "pulls":
-		isPull = util.OptionalBoolTrue
+		isPull = optional.Some(true)
 	case "issues":
-		isPull = util.OptionalBoolFalse
-	default:
-		isPull = util.OptionalBoolNone
+		isPull = optional.Some(false)
 	}
 
 	// FIXME: we should be more efficient here
@@ -2826,10 +2835,10 @@ func ListIssues(ctx *context.Context) {
 		SortBy:         issue_indexer.SortByCreatedDesc,
 	}
 	if since != 0 {
-		searchOpt.UpdatedAfterUnix = &since
+		searchOpt.UpdatedAfterUnix = optional.Some(since)
 	}
 	if before != 0 {
-		searchOpt.UpdatedBeforeUnix = &before
+		searchOpt.UpdatedBeforeUnix = optional.Some(before)
 	}
 	if len(labelIDs) == 1 && labelIDs[0] == 0 {
 		searchOpt.NoLabelOnly = true
@@ -2850,13 +2859,13 @@ func ListIssues(ctx *context.Context) {
 	}
 
 	if createdByID > 0 {
-		searchOpt.PosterID = &createdByID
+		searchOpt.PosterID = optional.Some(createdByID)
 	}
 	if assignedByID > 0 {
-		searchOpt.AssigneeID = &assignedByID
+		searchOpt.AssigneeID = optional.Some(assignedByID)
 	}
 	if mentionedByID > 0 {
-		searchOpt.MentionID = &mentionedByID
+		searchOpt.MentionID = optional.Some(mentionedByID)
 	}
 
 	ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
@@ -2871,7 +2880,7 @@ func ListIssues(ctx *context.Context) {
 	}
 
 	ctx.SetTotalCountHeader(total)
-	ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues))
+	ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues))
 }
 
 func BatchDeleteIssues(ctx *context.Context) {
@@ -3105,7 +3114,11 @@ func NewComment(ctx *context.Context) {
 
 	comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
 	if err != nil {
-		ctx.ServerError("CreateIssueComment", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+		} else {
+			ctx.ServerError("CreateIssueComment", err)
+		}
 		return
 	}
 
@@ -3149,7 +3162,11 @@ func UpdateCommentContent(ctx *context.Context) {
 		return
 	}
 	if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
-		ctx.ServerError("UpdateComment", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+		} else {
+			ctx.ServerError("UpdateComment", err)
+		}
 		return
 	}
 
@@ -3257,9 +3274,9 @@ func ChangeIssueReaction(ctx *context.Context) {
 
 	switch ctx.Params(":action") {
 	case "react":
-		reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content)
+		reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content)
 		if err != nil {
-			if issues_model.IsErrForbiddenIssueReaction(err) {
+			if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
 				ctx.ServerError("ChangeIssueReaction", err)
 				return
 			}
@@ -3301,7 +3318,7 @@ func ChangeIssueReaction(ctx *context.Context) {
 		return
 	}
 
-	html, err := ctx.RenderToString(tplReactions, map[string]any{
+	html, err := ctx.RenderToHTML(tplReactions, map[string]any{
 		"ctxData":   ctx.Data,
 		"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
 		"Reactions": issue.Reactions.GroupByType(),
@@ -3364,9 +3381,9 @@ func ChangeCommentReaction(ctx *context.Context) {
 
 	switch ctx.Params(":action") {
 	case "react":
-		reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content)
+		reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content)
 		if err != nil {
-			if issues_model.IsErrForbiddenIssueReaction(err) {
+			if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
 				ctx.ServerError("ChangeIssueReaction", err)
 				return
 			}
@@ -3408,7 +3425,7 @@ func ChangeCommentReaction(ctx *context.Context) {
 		return
 	}
 
-	html, err := ctx.RenderToString(tplReactions, map[string]any{
+	html, err := ctx.RenderToHTML(tplReactions, map[string]any{
 		"ctxData":   ctx.Data,
 		"ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
 		"Reactions": comment.Reactions.GroupByType(),
@@ -3551,8 +3568,8 @@ func updateAttachments(ctx *context.Context, item any, files []string) error {
 	return err
 }
 
-func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) string {
-	attachHTML, err := ctx.RenderToString(tplAttachment, map[string]any{
+func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML {
+	attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{
 		"ctxData":     ctx.Data,
 		"Attachments": attachments,
 		"Content":     content,
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index 0f376db145..bf3571c835 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -11,11 +11,11 @@ import (
 
 	"code.gitea.io/gitea/models/avatars"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/sergi/go-diff/diffmatchpatch"
 )
@@ -56,12 +56,12 @@ func GetContentHistoryList(ctx *context.Context) {
 	for _, item := range items {
 		var actionText string
 		if item.IsDeleted {
-			actionTextDeleted := ctx.Locale.Tr("repo.issues.content_history.deleted")
+			actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
 			actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
 		} else if item.IsFirstCreated {
-			actionText = ctx.Locale.Tr("repo.issues.content_history.created")
+			actionText = ctx.Locale.TrString("repo.issues.content_history.created")
 		} else {
-			actionText = ctx.Locale.Tr("repo.issues.content_history.edited")
+			actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
 		}
 
 		username := item.UserName
@@ -70,7 +70,7 @@ func GetContentHistoryList(ctx *context.Context) {
 		}
 
 		src := html.EscapeString(item.UserAvatarLink)
-		class := avatars.DefaultAvatarClass + " gt-mr-3"
+		class := avatars.DefaultAvatarClass + " tw-mr-2"
 		name := html.EscapeString(username)
 		avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
 		timeSinceText := string(timeutil.TimeSinceUnix(item.EditedUnix, ctx.Locale))
@@ -94,7 +94,7 @@ func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue
 	// CanWrite means the doer can manage the issue/PR list
 	if ctx.Repo.IsOwner() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
 		canSoftDelete = true
-	} else {
+	} else if ctx.Doer != nil {
 		// for read-only users, they could still post issues or comments,
 		// they should be able to delete the history related to their own issue/comment, a case is:
 		// 1. the user posts some sensitive data
@@ -186,6 +186,10 @@ func SoftDeleteContentHistory(ctx *context.Context) {
 	if ctx.Written() {
 		return
 	}
+	if ctx.Doer == nil {
+		ctx.NotFound("Require SignIn", nil)
+		return
+	}
 
 	commentID := ctx.FormInt64("comment_id")
 	historyID := ctx.FormInt64("history_id")
diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go
index 022ec3ae3e..e3b85ee638 100644
--- a/routers/web/repo/issue_dependency.go
+++ b/routers/web/repo/issue_dependency.go
@@ -8,8 +8,8 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 // AddDependency adds new dependencies
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index dd3e2803b4..81bee4dbb5 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -10,12 +10,11 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/label"
 	"code.gitea.io/gitea/modules/log"
 	repo_module "code.gitea.io/gitea/modules/repository"
-	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	issue_service "code.gitea.io/gitea/services/issue"
 )
@@ -112,12 +111,11 @@ func NewLabel(ctx *context.Context) {
 	}
 
 	l := &issues_model.Label{
-		RepoID:       ctx.Repo.Repository.ID,
-		Name:         form.Title,
-		Exclusive:    form.Exclusive,
-		Description:  form.Description,
-		Color:        form.Color,
-		ArchivedUnix: timeutil.TimeStamp(0),
+		RepoID:      ctx.Repo.Repository.ID,
+		Name:        form.Title,
+		Exclusive:   form.Exclusive,
+		Description: form.Description,
+		Color:       form.Color,
 	}
 	if err := issues_model.NewLabel(ctx, l); err != nil {
 		ctx.ServerError("NewLabel", err)
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index e0d49e44e1..93fc72300b 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -10,10 +10,10 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/stretchr/testify/assert"
@@ -123,7 +123,7 @@ func TestDeleteLabel(t *testing.T) {
 	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
 	unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
 	unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
-	assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
+	assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
 }
 
 func TestUpdateIssueLabel_Clear(t *testing.T) {
diff --git a/routers/web/repo/issue_lock.go b/routers/web/repo/issue_lock.go
index f83109d9b3..1d5fc8a5f3 100644
--- a/routers/web/repo/issue_lock.go
+++ b/routers/web/repo/issue_lock.go
@@ -5,8 +5,8 @@ package repo
 
 import (
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/repo/issue_pin.go b/routers/web/repo/issue_pin.go
index 9f334129f9..365c812681 100644
--- a/routers/web/repo/issue_pin.go
+++ b/routers/web/repo/issue_pin.go
@@ -7,9 +7,9 @@ import (
 	"net/http"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 // IssuePinOrUnpin pin or unpin a Issue
diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
index ab9fe3e69d..70d42b27c0 100644
--- a/routers/web/repo/issue_stopwatch.go
+++ b/routers/web/repo/issue_stopwatch.go
@@ -9,8 +9,8 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/eventsource"
+	"code.gitea.io/gitea/services/context"
 )
 
 // IssueStopwatch creates or stops a stopwatch for the given issue.
diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go
index c9bf861b84..241e434049 100644
--- a/routers/web/repo/issue_timetrack.go
+++ b/routers/web/repo/issue_timetrack.go
@@ -9,9 +9,9 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go
index 1f51ceba5e..8b033f3b17 100644
--- a/routers/web/repo/issue_watch.go
+++ b/routers/web/repo/issue_watch.go
@@ -9,8 +9,8 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go
index d70a53030e..420931c5fb 100644
--- a/routers/web/repo/middlewares.go
+++ b/routers/web/repo/middlewares.go
@@ -9,9 +9,9 @@ import (
 
 	system_model "code.gitea.io/gitea/models/system"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/optional"
+	"code.gitea.io/gitea/services/context"
 	user_service "code.gitea.io/gitea/services/user"
 )
 
diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go
index b70901d5f2..97b0c425ea 100644
--- a/routers/web/repo/migrate.go
+++ b/routers/web/repo/migrate.go
@@ -15,13 +15,13 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/migrations"
 	"code.gitea.io/gitea/services/task"
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index 19db2abd68..95a4fe60cc 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -12,13 +12,13 @@ import (
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/issue"
 
@@ -51,7 +51,7 @@ func Milestones(ctx *context.Context) {
 			PageSize: setting.UI.IssuePagingNum,
 		},
 		RepoID:   ctx.Repo.Repository.ID,
-		IsClosed: util.OptionalBoolOf(isShowClosed),
+		IsClosed: optional.Some(isShowClosed),
 		SortType: sortType,
 		Name:     keyword,
 	})
@@ -106,8 +106,8 @@ func Milestones(ctx *context.Context) {
 	ctx.Data["IsShowClosed"] = isShowClosed
 
 	pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5)
-	pager.AddParam(ctx, "state", "State")
-	pager.AddParam(ctx, "q", "Keyword")
+	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
+	pager.AddParamString("q", keyword)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplMilestone)
@@ -292,10 +292,10 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
 	ctx.Data["Title"] = milestone.Name
 	ctx.Data["Milestone"] = milestone
 
-	issues(ctx, milestoneID, projectID, util.OptionalBoolNone)
+	issues(ctx, milestoneID, projectID, optional.None[bool]())
 
-	ret, _ := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
-	ctx.Data["NewIssueChooseTemplate"] = len(ret) > 0
+	ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+	ctx.Data["NewIssueChooseTemplate"] = len(ret.IssueTemplates) > 0
 
 	ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
 	ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index ac9e64d774..57e578da37 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -10,9 +10,9 @@ import (
 	"code.gitea.io/gitea/models/packages"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -37,7 +37,7 @@ func Packages(ctx *context.Context) {
 		RepoID:     ctx.Repo.Repository.ID,
 		Type:       packages.Type(packageType),
 		Name:       packages.SearchValue{Value: query},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		ctx.ServerError("SearchLatestVersions", err)
@@ -70,8 +70,8 @@ func Packages(ctx *context.Context) {
 	ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository
 
 	pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
-	pager.AddParam(ctx, "q", "Query")
-	pager.AddParam(ctx, "type", "PackageType")
+	pager.AddParamString("q", query)
+	pager.AddParamString("type", packageType)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplPackagesList)
diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go
index c04435cf1b..0dee02dd9c 100644
--- a/routers/web/repo/patch.go
+++ b/routers/web/repo/patch.go
@@ -10,10 +10,10 @@ import (
 	git_model "code.gitea.io/gitea/models/git"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/repository/files"
 )
@@ -79,7 +79,7 @@ func NewDiffPatchPost(ctx *context.Context) {
 	// `message` will be both the summary and message combined
 	message := strings.TrimSpace(form.CommitSummary)
 	if len(message) == 0 {
-		message = ctx.Tr("repo.editor.patch")
+		message = ctx.Locale.TrString("repo.editor.patch")
 	}
 
 	form.CommitMessage = strings.TrimSpace(form.CommitMessage)
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 4908bb796d..a2db1fc770 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -14,16 +14,16 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/perm"
 	project_model "code.gitea.io/gitea/models/project"
-	attachment_model "code.gitea.io/gitea/models/repo"
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
@@ -33,16 +33,17 @@ const (
 	tplProjectsView base.TplName = "repo/projects/view"
 )
 
-// MustEnableProjects check if projects are enabled in settings
-func MustEnableProjects(ctx *context.Context) {
+// MustEnableRepoProjects check if repo projects are enabled in settings
+func MustEnableRepoProjects(ctx *context.Context) {
 	if unit.TypeProjects.UnitGlobalDisabled() {
 		ctx.NotFound("EnableKanbanBoard", nil)
 		return
 	}
 
 	if ctx.Repo.Repository != nil {
-		if !ctx.Repo.CanRead(unit.TypeProjects) {
-			ctx.NotFound("MustEnableProjects", nil)
+		projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects)
+		if !ctx.Repo.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
+			ctx.NotFound("MustEnableRepoProjects", nil)
 			return
 		}
 	}
@@ -78,7 +79,7 @@ func Projects(ctx *context.Context) {
 			Page:     page,
 		},
 		RepoID:   repo.ID,
-		IsClosed: util.OptionalBoolOf(isShowClosed),
+		IsClosed: optional.Some(isShowClosed),
 		OrderBy:  project_model.GetSearchOrderByBySortType(sortType),
 		Type:     project_model.TypeRepository,
 		Title:    keyword,
@@ -117,7 +118,7 @@ func Projects(ctx *context.Context) {
 	}
 
 	pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
-	pager.AddParam(ctx, "state", "State")
+	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
 	ctx.Data["Page"] = pager
 
 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
@@ -314,10 +315,6 @@ func ViewProject(ctx *context.Context) {
 		return
 	}
 
-	if boards[0].ID == 0 {
-		boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
-	}
-
 	issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
 	if err != nil {
 		ctx.ServerError("LoadIssuesOfBoards", err)
@@ -325,10 +322,10 @@ func ViewProject(ctx *context.Context) {
 	}
 
 	if project.CardType != project_model.CardTypeTextOnly {
-		issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
+		issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
 		for _, issuesList := range issuesMap {
 			for _, issue := range issuesList {
-				if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
+				if issueAttachment, err := repo_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
 					issuesAttachmentMap[issue.ID] = issueAttachment
 				}
 			}
@@ -339,17 +336,17 @@ func ViewProject(ctx *context.Context) {
 	linkedPrsMap := make(map[int64][]*issues_model.Issue)
 	for _, issuesList := range issuesMap {
 		for _, issue := range issuesList {
-			var referencedIds []int64
+			var referencedIDs []int64
 			for _, comment := range issue.Comments {
 				if comment.RefIssueID != 0 && comment.RefIsPull {
-					referencedIds = append(referencedIds, comment.RefIssueID)
+					referencedIDs = append(referencedIDs, comment.RefIssueID)
 				}
 			}
 
-			if len(referencedIds) > 0 {
+			if len(referencedIDs) > 0 {
 				if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
-					IssueIDs: referencedIds,
-					IsPull:   util.OptionalBoolTrue,
+					IssueIDs: referencedIDs,
+					IsPull:   optional.Some(true),
 				}); err == nil {
 					linkedPrsMap[issue.ID] = linkedPrs
 				}
@@ -582,21 +579,6 @@ func SetDefaultProjectBoard(ctx *context.Context) {
 	ctx.JSONOK()
 }
 
-// UnSetDefaultProjectBoard unset default board for uncategorized issues/pulls
-func UnSetDefaultProjectBoard(ctx *context.Context) {
-	project, _ := checkProjectBoardChangePermissions(ctx)
-	if ctx.Written() {
-		return
-	}
-
-	if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil {
-		ctx.ServerError("SetDefaultBoard", err)
-		return
-	}
-
-	ctx.JSONOK()
-}
-
 // MoveIssues moves or keeps issues in a column and sorts them inside that column
 func MoveIssues(ctx *context.Context) {
 	if ctx.Doer == nil {
@@ -627,28 +609,19 @@ func MoveIssues(ctx *context.Context) {
 		return
 	}
 
-	var board *project_model.Board
+	board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+	if err != nil {
+		if project_model.IsErrProjectBoardNotExist(err) {
+			ctx.NotFound("ProjectBoardNotExist", nil)
+		} else {
+			ctx.ServerError("GetProjectBoard", err)
+		}
+		return
+	}
 
-	if ctx.ParamsInt64(":boardID") == 0 {
-		board = &project_model.Board{
-			ID:        0,
-			ProjectID: project.ID,
-			Title:     ctx.Tr("repo.projects.type.uncategorized"),
-		}
-	} else {
-		board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
-		if err != nil {
-			if project_model.IsErrProjectBoardNotExist(err) {
-				ctx.NotFound("ProjectBoardNotExist", nil)
-			} else {
-				ctx.ServerError("GetProjectBoard", err)
-			}
-			return
-		}
-		if board.ProjectID != project.ID {
-			ctx.NotFound("BoardNotInProject", nil)
-			return
-		}
+	if board.ProjectID != project.ID {
+		ctx.NotFound("BoardNotInProject", nil)
+		return
 	}
 
 	type movedIssuesForm struct {
diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/projects_test.go
index 6698d47028..479f8c55a2 100644
--- a/routers/web/repo/projects_test.go
+++ b/routers/web/repo/projects_test.go
@@ -7,7 +7,7 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 8b09ae5463..9b45fa23d5 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -10,7 +10,6 @@ import (
 	"fmt"
 	"html"
 	"net/http"
-	"net/url"
 	"strconv"
 	"strings"
 	"time"
@@ -20,37 +19,36 @@ import (
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	pull_model "code.gitea.io/gitea/models/pull"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/emoji"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	issue_template "code.gitea.io/gitea/modules/issue/template"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/utils"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	"code.gitea.io/gitea/services/automerge"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/gitdiff"
 	notify_service "code.gitea.io/gitea/services/notify"
 	pull_service "code.gitea.io/gitea/services/pull"
 	repo_service "code.gitea.io/gitea/services/repository"
+	user_service "code.gitea.io/gitea/services/user"
 
 	"github.com/gobwas/glob"
 )
 
 const (
-	tplFork        base.TplName = "repo/pulls/fork"
 	tplCompareDiff base.TplName = "repo/diff/compare"
 	tplPullCommits base.TplName = "repo/pulls/commits"
 	tplPullFiles   base.TplName = "repo/pulls/files"
@@ -109,213 +107,6 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
 	return repo
 }
 
-func getForkRepository(ctx *context.Context) *repo_model.Repository {
-	forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid"))
-	if ctx.Written() {
-		return nil
-	}
-
-	if forkRepo.IsEmpty {
-		log.Trace("Empty repository %-v", forkRepo)
-		ctx.NotFound("getForkRepository", nil)
-		return nil
-	}
-
-	if err := forkRepo.LoadOwner(ctx); err != nil {
-		ctx.ServerError("LoadOwner", err)
-		return nil
-	}
-
-	ctx.Data["repo_name"] = forkRepo.Name
-	ctx.Data["description"] = forkRepo.Description
-	ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
-	canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID)
-
-	ctx.Data["ForkRepo"] = forkRepo
-
-	ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID)
-	if err != nil {
-		ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
-		return nil
-	}
-	var orgs []*organization.Organization
-	for _, org := range ownedOrgs {
-		if forkRepo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) {
-			orgs = append(orgs, org)
-		}
-	}
-
-	traverseParentRepo := forkRepo
-	for {
-		if ctx.Doer.ID == traverseParentRepo.OwnerID {
-			canForkToUser = false
-		} else {
-			for i, org := range orgs {
-				if org.ID == traverseParentRepo.OwnerID {
-					orgs = append(orgs[:i], orgs[i+1:]...)
-					break
-				}
-			}
-		}
-
-		if !traverseParentRepo.IsFork {
-			break
-		}
-		traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
-		if err != nil {
-			ctx.ServerError("GetRepositoryByID", err)
-			return nil
-		}
-	}
-
-	ctx.Data["CanForkToUser"] = canForkToUser
-	ctx.Data["Orgs"] = orgs
-
-	if canForkToUser {
-		ctx.Data["ContextUser"] = ctx.Doer
-	} else if len(orgs) > 0 {
-		ctx.Data["ContextUser"] = orgs[0]
-	} else {
-		ctx.Data["CanForkRepo"] = false
-		ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
-		return nil
-	}
-
-	branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
-		RepoID: ctx.Repo.Repository.ID,
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
-		IsDeletedBranch: util.OptionalBoolFalse,
-		// Add it as the first option
-		ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch},
-	})
-	if err != nil {
-		ctx.ServerError("FindBranchNames", err)
-		return nil
-	}
-	ctx.Data["Branches"] = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
-
-	return forkRepo
-}
-
-// Fork render repository fork page
-func Fork(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("new_fork")
-
-	if ctx.Doer.CanForkRepo() {
-		ctx.Data["CanForkRepo"] = true
-	} else {
-		maxCreationLimit := ctx.Doer.MaxCreationLimit()
-		msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
-		ctx.Flash.Error(msg, true)
-	}
-
-	getForkRepository(ctx)
-	if ctx.Written() {
-		return
-	}
-
-	ctx.HTML(http.StatusOK, tplFork)
-}
-
-// ForkPost response for forking a repository
-func ForkPost(ctx *context.Context) {
-	form := web.GetForm(ctx).(*forms.CreateRepoForm)
-	ctx.Data["Title"] = ctx.Tr("new_fork")
-	ctx.Data["CanForkRepo"] = true
-
-	ctxUser := checkContextUser(ctx, form.UID)
-	if ctx.Written() {
-		return
-	}
-
-	forkRepo := getForkRepository(ctx)
-	if ctx.Written() {
-		return
-	}
-
-	ctx.Data["ContextUser"] = ctxUser
-
-	if ctx.HasError() {
-		ctx.HTML(http.StatusOK, tplFork)
-		return
-	}
-
-	var err error
-	traverseParentRepo := forkRepo
-	for {
-		if ctxUser.ID == traverseParentRepo.OwnerID {
-			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
-			return
-		}
-		repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
-		if repo != nil {
-			ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
-			return
-		}
-		if !traverseParentRepo.IsFork {
-			break
-		}
-		traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
-		if err != nil {
-			ctx.ServerError("GetRepositoryByID", err)
-			return
-		}
-	}
-
-	// Check if user is allowed to create repo's on the organization.
-	if ctxUser.IsOrganization() {
-		isAllowedToFork, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID)
-		if err != nil {
-			ctx.ServerError("CanCreateOrgRepo", err)
-			return
-		} else if !isAllowedToFork {
-			ctx.Error(http.StatusForbidden)
-			return
-		}
-	}
-
-	repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
-		BaseRepo:     forkRepo,
-		Name:         form.RepoName,
-		Description:  form.Description,
-		SingleBranch: form.ForkSingleBranch,
-	})
-	if err != nil {
-		ctx.Data["Err_RepoName"] = true
-		switch {
-		case repo_model.IsErrReachLimitOfRepo(err):
-			maxCreationLimit := ctxUser.MaxCreationLimit()
-			msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
-			ctx.RenderWithErr(msg, tplFork, &form)
-		case repo_model.IsErrRepoAlreadyExist(err):
-			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
-		case repo_model.IsErrRepoFilesAlreadyExist(err):
-			switch {
-			case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
-				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
-			case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
-				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
-			case setting.Repository.AllowDeleteOfUnadoptedRepositories:
-				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
-			default:
-				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
-			}
-		case db.IsErrNameReserved(err):
-			ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
-		case db.IsErrNamePatternNotAllowed(err):
-			ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
-		default:
-			ctx.ServerError("ForkPost", err)
-		}
-		return
-	}
-
-	log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
-	ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
-}
-
 func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
 	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
@@ -334,7 +125,7 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
 		ctx.ServerError("LoadRepo", err)
 		return nil, false
 	}
-	ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
+	ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title))
 	ctx.Data["Issue"] = issue
 
 	if !issue.IsPull {
@@ -487,7 +278,7 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
 
 	if len(compareInfo.Commits) != 0 {
 		sha := compareInfo.Commits[0].ID.String()
-		commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptions{ListAll: true})
+		commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll)
 		if err != nil {
 			ctx.ServerError("GetLatestCommitStatus", err)
 			return nil
@@ -549,7 +340,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
 			ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
 			return nil
 		}
-		commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true})
+		commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
 		if err != nil {
 			ctx.ServerError("GetLatestCommitStatus", err)
 			return nil
@@ -641,7 +432,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
 		return nil
 	}
 
-	commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true})
+	commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
 	if err != nil {
 		ctx.ServerError("GetLatestCommitStatus", err)
 		return nil
@@ -652,6 +443,24 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
 	}
 
 	if pb != nil && pb.EnableStatusCheck {
+
+		var missingRequiredChecks []string
+		for _, requiredContext := range pb.StatusCheckContexts {
+			contextFound := false
+			matchesRequiredContext := createRequiredContextMatcher(requiredContext)
+			for _, presentStatus := range commitStatuses {
+				if matchesRequiredContext(presentStatus.Context) {
+					contextFound = true
+					break
+				}
+			}
+
+			if !contextFound {
+				missingRequiredChecks = append(missingRequiredChecks, requiredContext)
+			}
+		}
+		ctx.Data["MissingRequiredChecks"] = missingRequiredChecks
+
 		ctx.Data["is_context_required"] = func(context string) bool {
 			for _, c := range pb.StatusCheckContexts {
 				if c == context {
@@ -720,10 +529,22 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
 	return compareInfo
 }
 
+func createRequiredContextMatcher(requiredContext string) func(string) bool {
+	if gp, err := glob.Compile(requiredContext); err == nil {
+		return func(contextToCheck string) bool {
+			return gp.Match(contextToCheck)
+		}
+	}
+
+	return func(contextToCheck string) bool {
+		return requiredContext == contextToCheck
+	}
+}
+
 type pullCommitList struct {
 	Commits             []pull_service.CommitInfo `json:"commits"`
 	LastReviewCommitSha string                    `json:"last_review_commit_sha"`
-	Locale              map[string]string         `json:"locale"`
+	Locale              map[string]any            `json:"locale"`
 }
 
 // GetPullCommits get all commits for given pull request
@@ -741,7 +562,7 @@ func GetPullCommits(ctx *context.Context) {
 	}
 
 	// Get the needed locale
-	resp.Locale = map[string]string{
+	resp.Locale = map[string]any{
 		"lang":                                ctx.Locale.Language(),
 		"show_all_commits":                    ctx.Tr("repo.pulls.show_all_commits"),
 		"stats_num_commits":                   ctx.TrN(len(commits), "repo.activity.git_stats_commit_1", "repo.activity.git_stats_commit_n", len(commits)),
@@ -938,6 +759,19 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 		return
 	}
 
+	for _, file := range diff.Files {
+		for _, section := range file.Sections {
+			for _, line := range section.Lines {
+				for _, comment := range line.Comments {
+					if err := comment.LoadAttachments(ctx); err != nil {
+						ctx.ServerError("LoadAttachments", err)
+						return
+					}
+				}
+			}
+		}
+	}
+
 	pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
 	if err != nil {
 		ctx.ServerError("LoadProtectedBranch", err)
@@ -1020,6 +854,36 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 	}
 	upload.AddUploadContext(ctx, "comment")
 
+	ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+		return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+	}
+	if !willShowSpecifiedCommit && !willShowSpecifiedCommitRange && pull.Flow == issues_model.PullRequestFlowGithub {
+		if err := pull.LoadHeadRepo(ctx); err != nil {
+			ctx.ServerError("LoadHeadRepo", err)
+			return
+		}
+
+		if pull.HeadRepo != nil {
+			ctx.Data["SourcePath"] = pull.HeadRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pull.HeadBranch)
+		}
+
+		if !pull.HasMerged && ctx.Doer != nil {
+			perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
+			if err != nil {
+				ctx.ServerError("GetUserRepoPermission", err)
+				return
+			}
+
+			if perm.CanWrite(unit.TypeCode) || issues_model.CanMaintainerWriteToBranch(ctx, perm, pull.HeadBranch, ctx.Doer) {
+				ctx.Data["CanEditFile"] = true
+				ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
+				ctx.Data["HeadRepoLink"] = pull.HeadRepo.Link()
+				ctx.Data["HeadBranchName"] = pull.HeadBranch
+				ctx.Data["BackToLink"] = setting.AppSubURL + ctx.Req.URL.RequestURI()
+			}
+		}
+	}
+
 	ctx.HTML(http.StatusOK, tplPullFiles)
 }
 
@@ -1084,7 +948,7 @@ func UpdatePullRequest(ctx *context.Context) {
 	if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil {
 		if models.IsErrMergeConflicts(err) {
 			conflictError := err.(models.ErrMergeConflicts)
-			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 				"Message": ctx.Tr("repo.pulls.merge_conflict"),
 				"Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
 				"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1098,7 +962,7 @@ func UpdatePullRequest(ctx *context.Context) {
 			return
 		} else if models.IsErrRebaseConflicts(err) {
 			conflictError := err.(models.ErrRebaseConflicts)
-			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 				"Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
 				"Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
 				"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1230,7 +1094,7 @@ func MergePullRequest(ctx *context.Context) {
 			ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
 		} else if models.IsErrMergeConflicts(err) {
 			conflictError := err.(models.ErrMergeConflicts)
-			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 				"Message": ctx.Tr("repo.editor.merge_conflict"),
 				"Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
 				"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1243,7 +1107,7 @@ func MergePullRequest(ctx *context.Context) {
 			ctx.JSONRedirect(issue.Link())
 		} else if models.IsErrRebaseConflicts(err) {
 			conflictError := err.(models.ErrRebaseConflicts)
-			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 				"Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
 				"Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
 				"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1253,19 +1117,19 @@ func MergePullRequest(ctx *context.Context) {
 				return
 			}
 			ctx.Flash.Error(flashError)
-			ctx.Redirect(issue.Link())
+			ctx.JSONRedirect(issue.Link())
 		} else if models.IsErrMergeUnrelatedHistories(err) {
 			log.Debug("MergeUnrelatedHistories error: %v", err)
 			ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories"))
-			ctx.Redirect(issue.Link())
+			ctx.JSONRedirect(issue.Link())
 		} else if git.IsErrPushOutOfDate(err) {
 			log.Debug("MergePushOutOfDate error: %v", err)
 			ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
-			ctx.Redirect(issue.Link())
+			ctx.JSONRedirect(issue.Link())
 		} else if models.IsErrSHADoesNotMatch(err) {
 			log.Debug("MergeHeadOutOfDate error: %v", err)
 			ctx.Flash.Error(ctx.Tr("repo.pulls.head_out_of_date"))
-			ctx.Redirect(issue.Link())
+			ctx.JSONRedirect(issue.Link())
 		} else if git.IsErrPushRejected(err) {
 			log.Debug("MergePushRejected error: %v", err)
 			pushrejErr := err.(*git.ErrPushRejected)
@@ -1273,7 +1137,7 @@ func MergePullRequest(ctx *context.Context) {
 			if len(message) == 0 {
 				ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
 			} else {
-				flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+				flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 					"Message": ctx.Tr("repo.pulls.push_rejected"),
 					"Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
 					"Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
@@ -1438,7 +1302,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 	if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
 		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
-			return
 		} else if git.IsErrPushRejected(err) {
 			pushrejErr := err.(*git.ErrPushRejected)
 			message := pushrejErr.Message
@@ -1446,7 +1309,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 				ctx.JSONError(ctx.Tr("repo.pulls.push_rejected_no_message"))
 				return
 			}
-			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
 				"Message": ctx.Tr("repo.pulls.push_rejected"),
 				"Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
 				"Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
@@ -1455,11 +1318,18 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 				ctx.ServerError("CompareAndPullRequest.HTMLString", err)
 				return
 			}
-			ctx.Flash.Error(flashError)
-			ctx.JSONRedirect(pullIssue.Link()) // FIXME: it's unfriendly, and will make the content lost
-			return
+			ctx.JSONError(flashError)
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
+				"Message": ctx.Tr("repo.pulls.push_rejected"),
+				"Summary": ctx.Tr("repo.pulls.new.blocked_user"),
+			})
+			if err != nil {
+				ctx.ServerError("CompareAndPullRequest.HTMLString", err)
+				return
+			}
+			ctx.JSONError(flashError)
 		}
-		ctx.ServerError("NewPullRequest", err)
 		return
 	}
 
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index b93460d169..c8d149a482 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -10,14 +10,17 @@ import (
 
 	issues_model "code.gitea.io/gitea/models/issues"
 	pull_model "code.gitea.io/gitea/models/pull"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/forms"
 	pull_service "code.gitea.io/gitea/services/pull"
+	user_service "code.gitea.io/gitea/services/user"
 )
 
 const (
@@ -50,6 +53,8 @@ func RenderNewCodeCommentForm(ctx *context.Context) {
 		return
 	}
 	ctx.Data["AfterCommitID"] = pullHeadCommitID
+	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+	upload.AddUploadContext(ctx, "comment")
 	ctx.HTML(http.StatusOK, tplNewComment)
 }
 
@@ -75,6 +80,11 @@ func CreateCodeComment(ctx *context.Context) {
 		signedLine *= -1
 	}
 
+	var attachments []string
+	if setting.Attachment.Enabled {
+		attachments = form.Files
+	}
+
 	comment, err := pull_service.CreateCodeComment(ctx,
 		ctx.Doer,
 		ctx.Repo.GitRepo,
@@ -85,6 +95,7 @@ func CreateCodeComment(ctx *context.Context) {
 		!form.SingleReview,
 		form.Reply,
 		form.LatestCommitID,
+		attachments,
 	)
 	if err != nil {
 		ctx.ServerError("CreateCodeComment", err)
@@ -156,7 +167,8 @@ func UpdateResolveConversation(ctx *context.Context) {
 func renderConversation(ctx *context.Context, comment *issues_model.Comment, origin string) {
 	ctx.Data["PageIsPullFiles"] = origin == "diff"
 
-	comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, ctx.Data["ShowOutdatedComments"].(bool))
+	showOutdatedComments := origin == "timeline" || ctx.Data["ShowOutdatedComments"].(bool)
+	comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, showOutdatedComments)
 	if err != nil {
 		ctx.ServerError("FetchCodeCommentsByLine", err)
 		return
@@ -167,6 +179,14 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
 		return
 	}
 
+	if err := comments.LoadAttachments(ctx); err != nil {
+		ctx.ServerError("LoadAttachments", err)
+		return
+	}
+
+	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+	upload.AddUploadContext(ctx, "comment")
+
 	ctx.Data["comments"] = comments
 	if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, comment.Issue, ctx.Doer); err != nil {
 		ctx.ServerError("CanMarkConversation", err)
@@ -183,6 +203,10 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
 		return
 	}
 	ctx.Data["AfterCommitID"] = pullHeadCommitID
+	ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+		return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+	}
+
 	if origin == "diff" {
 		ctx.HTML(http.StatusOK, tplDiffConversation)
 	} else if origin == "timeline" {
@@ -219,9 +243,9 @@ func SubmitReview(ctx *context.Context) {
 		if issue.IsPoster(ctx.Doer.ID) {
 			var translated string
 			if reviewType == issues_model.ReviewTypeApprove {
-				translated = ctx.Tr("repo.issues.review.self.approval")
+				translated = ctx.Locale.TrString("repo.issues.review.self.approval")
 			} else {
-				translated = ctx.Tr("repo.issues.review.self.rejection")
+				translated = ctx.Locale.TrString("repo.issues.review.self.rejection")
 			}
 
 			ctx.Flash.Error(translated)
@@ -253,6 +277,10 @@ func DismissReview(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.DismissReviewForm)
 	comm, err := pull_service.DismissReview(ctx, form.ReviewID, ctx.Repo.Repository.ID, form.Message, ctx.Doer, true, true)
 	if err != nil {
+		if pull_service.IsErrDismissRequestOnClosedPR(err) {
+			ctx.Status(http.StatusForbidden)
+			return
+		}
 		ctx.ServerError("pull_service.DismissReview", err)
 		return
 	}
diff --git a/routers/web/repo/pull_review_test.go b/routers/web/repo/pull_review_test.go
index 65019af40b..8344ff4091 100644
--- a/routers/web/repo/pull_review_test.go
+++ b/routers/web/repo/pull_review_test.go
@@ -4,15 +4,16 @@
 package repo
 
 import (
+	"net/http"
 	"net/http/httptest"
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/context"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/templates"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/pull"
 
 	"github.com/stretchr/testify/assert"
@@ -39,7 +40,7 @@ func TestRenderConversation(t *testing.T) {
 
 	var preparedComment *issues_model.Comment
 	run("prepare", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
-		comment, err := pull.CreateCodeComment(ctx, pr.Issue.Poster, ctx.Repo.GitRepo, pr.Issue, 1, "content", "", false, 0, pr.HeadCommitID)
+		comment, err := pull.CreateCodeComment(ctx, pr.Issue.Poster, ctx.Repo.GitRepo, pr.Issue, 1, "content", "", false, 0, pr.HeadCommitID, nil)
 		if !assert.NoError(t, err) {
 			return
 		}
@@ -68,9 +69,25 @@ func TestRenderConversation(t *testing.T) {
 		renderConversation(ctx, preparedComment, "timeline")
 		assert.Contains(t, resp.Body.String(), `<div id="code-comments-`)
 	})
-	run("timeline without outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
+	run("timeline is not affected by ShowOutdatedComments=false", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
 		ctx.Data["ShowOutdatedComments"] = false
 		renderConversation(ctx, preparedComment, "timeline")
-		assert.Contains(t, resp.Body.String(), `conversation-not-existing`)
+		assert.Contains(t, resp.Body.String(), `<div id="code-comments-`)
+	})
+	run("diff non-existing review", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
+		err := db.TruncateBeans(db.DefaultContext, &issues_model.Review{})
+		assert.NoError(t, err)
+		ctx.Data["ShowOutdatedComments"] = true
+		renderConversation(ctx, preparedComment, "diff")
+		assert.Equal(t, http.StatusOK, resp.Code)
+		assert.NotContains(t, resp.Body.String(), `status-page-500`)
+	})
+	run("timeline non-existing review", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
+		err := db.TruncateBeans(db.DefaultContext, &issues_model.Review{})
+		assert.NoError(t, err)
+		ctx.Data["ShowOutdatedComments"] = true
+		renderConversation(ctx, preparedComment, "timeline")
+		assert.Equal(t, http.StatusOK, resp.Code)
+		assert.NotContains(t, resp.Body.String(), `status-page-500`)
 	})
 }
diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go
new file mode 100644
index 0000000000..c158fb30b6
--- /dev/null
+++ b/routers/web/repo/recent_commits.go
@@ -0,0 +1,41 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+	"errors"
+	"net/http"
+
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/services/context"
+	contributors_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+	tplRecentCommits base.TplName = "repo/activity"
+)
+
+// RecentCommits renders the page to show recent commit frequency on repository
+func RecentCommits(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.recent_commits")
+
+	ctx.Data["PageIsActivity"] = true
+	ctx.Data["PageIsRecentCommits"] = true
+	ctx.PageData["repoLink"] = ctx.Repo.RepoLink
+
+	ctx.HTML(http.StatusOK, tplRecentCommits)
+}
+
+// RecentCommitsData returns JSON of recent commits data
+func RecentCommitsData(ctx *context.Context) {
+	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+		if errors.Is(err, contributors_service.ErrAwaitGeneration) {
+			ctx.Status(http.StatusAccepted)
+			return
+		}
+		ctx.ServerError("RecentCommitsData", err)
+	} else {
+		ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
+	}
+}
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index fdb247d413..7ba23f0701 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -12,20 +12,22 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/web/feed"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context/upload"
 	"code.gitea.io/gitea/services/forms"
 	releaseservice "code.gitea.io/gitea/services/release"
 )
@@ -67,6 +69,88 @@ func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model
 	return nil
 }
 
+type ReleaseInfo struct {
+	Release        *repo_model.Release
+	CommitStatus   *git_model.CommitStatus
+	CommitStatuses []*git_model.CommitStatus
+}
+
+func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) ([]*ReleaseInfo, error) {
+	releases, err := db.Find[repo_model.Release](ctx, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, release := range releases {
+		release.Repo = ctx.Repo.Repository
+	}
+
+	if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil {
+		return nil, err
+	}
+
+	// Temporary cache commits count of used branches to speed up.
+	countCache := make(map[string]int64)
+	cacheUsers := make(map[int64]*user_model.User)
+	if ctx.Doer != nil {
+		cacheUsers[ctx.Doer.ID] = ctx.Doer
+	}
+	var ok bool
+
+	canReadActions := ctx.Repo.CanRead(unit.TypeActions)
+
+	releaseInfos := make([]*ReleaseInfo, 0, len(releases))
+	for _, r := range releases {
+		if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
+			r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
+			if err != nil {
+				if user_model.IsErrUserNotExist(err) {
+					r.Publisher = user_model.NewGhostUser()
+				} else {
+					return nil, err
+				}
+			}
+			cacheUsers[r.PublisherID] = r.Publisher
+		}
+
+		r.RenderedNote, err = markdown.RenderString(&markup.RenderContext{
+			Links: markup.Links{
+				Base: ctx.Repo.RepoLink,
+			},
+			Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
+			GitRepo: ctx.Repo.GitRepo,
+			Ctx:     ctx,
+		}, r.Note)
+		if err != nil {
+			return nil, err
+		}
+
+		if !r.IsDraft {
+			if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
+				return nil, err
+			}
+		}
+
+		info := &ReleaseInfo{
+			Release: r,
+		}
+
+		if canReadActions {
+			statuses, _, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptionsAll)
+			if err != nil {
+				return nil, err
+			}
+
+			info.CommitStatus = git_model.CalcCommitStatus(statuses)
+			info.CommitStatuses = statuses
+		}
+
+		releaseInfos = append(releaseInfos, info)
+	}
+
+	return releaseInfos, nil
+}
+
 // Releases render releases list page
 func Releases(ctx *context.Context) {
 	ctx.Data["PageIsReleaseList"] = true
@@ -91,77 +175,26 @@ func Releases(ctx *context.Context) {
 	writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
 	ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
 
-	opts := repo_model.FindReleasesOptions{
+	releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
 		ListOptions: listOptions,
 		// only show draft releases for users who can write, read-only users shouldn't see draft releases.
 		IncludeDrafts: writeAccess,
 		RepoID:        ctx.Repo.Repository.ID,
-	}
-
-	releases, err := db.Find[repo_model.Release](ctx, opts)
+	})
 	if err != nil {
-		ctx.ServerError("GetReleasesByRepoID", err)
+		ctx.ServerError("getReleaseInfos", err)
 		return
 	}
-
-	for _, release := range releases {
-		release.Repo = ctx.Repo.Repository
-	}
-
-	if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil {
-		ctx.ServerError("GetReleaseAttachments", err)
-		return
-	}
-
-	// Temporary cache commits count of used branches to speed up.
-	countCache := make(map[string]int64)
-	cacheUsers := make(map[int64]*user_model.User)
-	if ctx.Doer != nil {
-		cacheUsers[ctx.Doer.ID] = ctx.Doer
-	}
-	var ok bool
-
-	for _, r := range releases {
-		if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
-			r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
-			if err != nil {
-				if user_model.IsErrUserNotExist(err) {
-					r.Publisher = user_model.NewGhostUser()
-				} else {
-					ctx.ServerError("GetUserByID", err)
-					return
-				}
-			}
-			cacheUsers[r.PublisherID] = r.Publisher
-		}
-
-		r.Note, err = markdown.RenderString(&markup.RenderContext{
-			Links: markup.Links{
-				Base: ctx.Repo.RepoLink,
-			},
-			Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
-			GitRepo: ctx.Repo.GitRepo,
-			Ctx:     ctx,
-		}, r.Note)
-		if err != nil {
-			ctx.ServerError("RenderString", err)
-			return
-		}
-
-		if r.IsDraft {
-			continue
-		}
-
-		if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
-			ctx.ServerError("calReleaseNumCommitsBehind", err)
-			return
+	for _, rel := range releases {
+		if rel.Release.IsTag && rel.Release.Title == "" {
+			rel.Release.Title = rel.Release.TagName
 		}
 	}
 
 	ctx.Data["Releases"] = releases
 
 	numReleases := ctx.Data["NumReleases"].(int64)
-	pager := context.NewPagination(int(numReleases), opts.PageSize, opts.Page, 5)
+	pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5)
 	pager.SetDefaultParams(ctx)
 	ctx.Data["Page"] = pager
 
@@ -196,7 +229,7 @@ func TagsList(ctx *context.Context) {
 		// the drafts should also be included because a real tag might be used as a draft.
 		IncludeDrafts: true,
 		IncludeTags:   true,
-		HasSha1:       util.OptionalBoolTrue,
+		HasSha1:       optional.Some(true),
 		RepoID:        ctx.Repo.Repository.ID,
 	}
 
@@ -249,15 +282,28 @@ func SingleRelease(ctx *context.Context) {
 	writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
 	ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
 
-	release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, ctx.Params("*"))
+	releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
+		ListOptions: db.ListOptions{Page: 1, PageSize: 1},
+		RepoID:      ctx.Repo.Repository.ID,
+		TagNames:    []string{ctx.Params("*")},
+		// only show draft releases for users who can write, read-only users shouldn't see draft releases.
+		IncludeDrafts: writeAccess,
+		IncludeTags:   true,
+	})
 	if err != nil {
-		if repo_model.IsErrReleaseNotExist(err) {
-			ctx.NotFound("GetRelease", err)
-			return
-		}
-		ctx.ServerError("GetReleasesByRepoID", err)
+		ctx.ServerError("getReleaseInfos", err)
 		return
 	}
+	if len(releases) != 1 {
+		ctx.NotFound("SingleRelease", err)
+		return
+	}
+
+	release := releases[0].Release
+	if release.IsTag && release.Title == "" {
+		release.Title = release.TagName
+	}
+
 	ctx.Data["PageIsSingleTag"] = release.IsTag
 	if release.IsTag {
 		ctx.Data["Title"] = release.TagName
@@ -265,43 +311,7 @@ func SingleRelease(ctx *context.Context) {
 		ctx.Data["Title"] = release.Title
 	}
 
-	release.Repo = ctx.Repo.Repository
-
-	err = repo_model.GetReleaseAttachments(ctx, release)
-	if err != nil {
-		ctx.ServerError("GetReleaseAttachments", err)
-		return
-	}
-
-	release.Publisher, err = user_model.GetUserByID(ctx, release.PublisherID)
-	if err != nil {
-		if user_model.IsErrUserNotExist(err) {
-			release.Publisher = user_model.NewGhostUser()
-		} else {
-			ctx.ServerError("GetUserByID", err)
-			return
-		}
-	}
-	if !release.IsDraft {
-		if err := calReleaseNumCommitsBehind(ctx.Repo, release, make(map[string]int64)); err != nil {
-			ctx.ServerError("calReleaseNumCommitsBehind", err)
-			return
-		}
-	}
-	release.Note, err = markdown.RenderString(&markup.RenderContext{
-		Links: markup.Links{
-			Base: ctx.Repo.RepoLink,
-		},
-		Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
-		GitRepo: ctx.Repo.GitRepo,
-		Ctx:     ctx,
-	}, release.Note)
-	if err != nil {
-		ctx.ServerError("RenderString", err)
-		return
-	}
-
-	ctx.Data["Releases"] = []*repo_model.Release{release}
+	ctx.Data["Releases"] = releases
 	ctx.HTML(http.StatusOK, tplReleasesList)
 }
 
diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go
index c4a2c1904e..7ebea4c3fb 100644
--- a/routers/web/repo/release_test.go
+++ b/routers/web/repo/release_test.go
@@ -10,8 +10,8 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/stretchr/testify/assert"
diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go
index 7eb5a42aa4..e64db03e20 100644
--- a/routers/web/repo/render.go
+++ b/routers/web/repo/render.go
@@ -10,11 +10,12 @@ import (
 	"path"
 
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/typesniffer"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 // RenderFile renders a file by repos path
@@ -44,20 +45,17 @@ func RenderFile(ctx *context.Context) {
 	isTextFile := st.IsText()
 
 	rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
+	ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
 
 	if markupType := markup.Type(blob.Name()); markupType == "" {
 		if isTextFile {
-			_, err = io.Copy(ctx.Resp, rd)
-			if err != nil {
-				ctx.ServerError("Copy", err)
-			}
-			return
+			_, _ = io.Copy(ctx.Resp, rd)
+		} else {
+			http.Error(ctx.Resp, "Unsupported file type render", http.StatusInternalServerError)
 		}
-		ctx.Error(http.StatusInternalServerError, "Unsupported file type render")
 		return
 	}
 
-	ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
 	err = markup.Render(&markup.RenderContext{
 		Ctx:          ctx,
 		RelativePath: ctx.Repo.TreePath,
@@ -71,7 +69,8 @@ func RenderFile(ctx *context.Context) {
 		InStandalonePage: true,
 	}, rd, ctx.Resp)
 	if err != nil {
-		ctx.ServerError("Render", err)
+		log.Error("Failed to render file %q: %v", ctx.Repo.TreePath, err)
+		http.Error(ctx.Resp, "Failed to render file", http.StatusInternalServerError)
 		return
 	}
 }
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index bede21be17..4e448933c7 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -21,19 +21,21 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/cache"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	repo_service "code.gitea.io/gitea/services/repository"
 	archiver_service "code.gitea.io/gitea/services/repository/archiver"
+	commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
 )
 
 const (
@@ -243,7 +245,7 @@ func CreatePost(ctx *context.Context) {
 	var repo *repo_model.Repository
 	var err error
 	if form.RepoTemplate > 0 {
-		opts := repo_module.GenerateRepoOptions{
+		opts := repo_service.GenerateRepoOptions{
 			Name:            form.RepoName,
 			Description:     form.Description,
 			Private:         form.Private,
@@ -312,13 +314,13 @@ func Action(ctx *context.Context) {
 	var err error
 	switch ctx.Params(":action") {
 	case "watch":
-		err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+		err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
 	case "unwatch":
-		err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+		err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
 	case "star":
-		err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+		err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
 	case "unstar":
-		err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+		err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
 	case "accept_transfer":
 		err = acceptOrRejectRepoTransfer(ctx, true)
 	case "reject_transfer":
@@ -335,8 +337,12 @@ func Action(ctx *context.Context) {
 	}
 
 	if err != nil {
-		ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
-		return
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
+		} else {
+			ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
+			return
+		}
 	}
 
 	switch ctx.Params(":action") {
@@ -365,7 +371,7 @@ func Action(ctx *context.Context) {
 		return
 	}
 
-	ctx.RedirectToFirst(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
+	ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
 }
 
 func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
@@ -541,9 +547,13 @@ func InitiateDownload(ctx *context.Context) {
 
 // SearchRepo repositories via options
 func SearchRepo(ctx *context.Context) {
+	page := ctx.FormInt("page")
+	if page <= 0 {
+		page = 1
+	}
 	opts := &repo_model.SearchRepoOptions{
 		ListOptions: db.ListOptions{
-			Page:     ctx.FormInt("page"),
+			Page:     page,
 			PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 		},
 		Actor:              ctx.Doer,
@@ -552,33 +562,33 @@ func SearchRepo(ctx *context.Context) {
 		PriorityOwnerID:    ctx.FormInt64("priority_owner_id"),
 		TeamID:             ctx.FormInt64("team_id"),
 		TopicOnly:          ctx.FormBool("topic"),
-		Collaborate:        util.OptionalBoolNone,
+		Collaborate:        optional.None[bool](),
 		Private:            ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
-		Template:           util.OptionalBoolNone,
+		Template:           optional.None[bool](),
 		StarredByID:        ctx.FormInt64("starredBy"),
 		IncludeDescription: ctx.FormBool("includeDesc"),
 	}
 
 	if ctx.FormString("template") != "" {
-		opts.Template = util.OptionalBoolOf(ctx.FormBool("template"))
+		opts.Template = optional.Some(ctx.FormBool("template"))
 	}
 
 	if ctx.FormBool("exclusive") {
-		opts.Collaborate = util.OptionalBoolFalse
+		opts.Collaborate = optional.Some(false)
 	}
 
 	mode := ctx.FormString("mode")
 	switch mode {
 	case "source":
-		opts.Fork = util.OptionalBoolFalse
-		opts.Mirror = util.OptionalBoolFalse
+		opts.Fork = optional.Some(false)
+		opts.Mirror = optional.Some(false)
 	case "fork":
-		opts.Fork = util.OptionalBoolTrue
+		opts.Fork = optional.Some(true)
 	case "mirror":
-		opts.Mirror = util.OptionalBoolTrue
+		opts.Mirror = optional.Some(true)
 	case "collaborative":
-		opts.Mirror = util.OptionalBoolFalse
-		opts.Collaborate = util.OptionalBoolTrue
+		opts.Mirror = optional.Some(false)
+		opts.Collaborate = optional.Some(true)
 	case "":
 	default:
 		ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode))
@@ -586,11 +596,11 @@ func SearchRepo(ctx *context.Context) {
 	}
 
 	if ctx.FormString("archived") != "" {
-		opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived"))
+		opts.Archived = optional.Some(ctx.FormBool("archived"))
 	}
 
 	if ctx.FormString("is_private") != "" {
-		opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private"))
+		opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
 	}
 
 	sortMode := ctx.FormString("sort")
@@ -612,47 +622,36 @@ func SearchRepo(ctx *context.Context) {
 		}
 	}
 
-	var err error
+	// To improve performance when only the count is requested
+	if ctx.FormBool("count_only") {
+		if count, err := repo_model.CountRepository(ctx, opts); err != nil {
+			log.Error("CountRepository: %v", err)
+			ctx.JSON(http.StatusInternalServerError, nil) // frontend JS doesn't handle error response (same as below)
+		} else {
+			ctx.SetTotalCountHeader(count)
+			ctx.JSONOK()
+		}
+		return
+	}
+
 	repos, count, err := repo_model.SearchRepository(ctx, opts)
 	if err != nil {
-		ctx.JSON(http.StatusInternalServerError, api.SearchError{
-			OK:    false,
-			Error: err.Error(),
-		})
+		log.Error("SearchRepository: %v", err)
+		ctx.JSON(http.StatusInternalServerError, nil)
 		return
 	}
 
 	ctx.SetTotalCountHeader(count)
 
-	// To improve performance when only the count is requested
-	if ctx.FormBool("count_only") {
-		return
-	}
-
-	// collect the latest commit of each repo
-	// at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment
-	repoBranchNames := make(map[int64]string, len(repos))
-	for _, repo := range repos {
-		repoBranchNames[repo.ID] = repo.DefaultBranch
-	}
-
-	repoIDsToLatestCommitSHAs, err := git_model.FindBranchesByRepoAndBranchName(ctx, repoBranchNames)
+	latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(ctx, repos)
 	if err != nil {
-		log.Error("FindBranchesByRepoAndBranchName: %v", err)
-		return
-	}
-
-	// call the database O(1) times to get the commit statuses for all repos
-	repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptionsAll)
-	if err != nil {
-		log.Error("GetLatestCommitStatusForPairs: %v", err)
+		log.Error("FindReposLastestCommitStatuses: %v", err)
+		ctx.JSON(http.StatusInternalServerError, nil)
 		return
 	}
 
 	results := make([]*repo_service.WebSearchRepository, len(repos))
 	for i, repo := range repos {
-		latestCommitStatus := git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
-
 		results[i] = &repo_service.WebSearchRepository{
 			Repository: &api.Repository{
 				ID:       repo.ID,
@@ -666,8 +665,11 @@ func SearchRepo(ctx *context.Context) {
 				Link:     repo.Link(),
 				Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
 			},
-			LatestCommitStatus:       latestCommitStatus,
-			LocaleLatestCommitStatus: latestCommitStatus.LocaleString(ctx.Locale),
+		}
+
+		if latestCommitStatuses[i] != nil {
+			results[i].LatestCommitStatus = latestCommitStatuses[i]
+			results[i].LocaleLatestCommitStatus = latestCommitStatuses[i].LocaleString(ctx.Locale)
 		}
 	}
 
@@ -685,10 +687,8 @@ type branchTagSearchResponse struct {
 func GetBranchesList(ctx *context.Context) {
 	branchOpts := git_model.FindBranchOptions{
 		RepoID:          ctx.Repo.Repository.ID,
-		IsDeletedBranch: util.OptionalBoolFalse,
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		IsDeletedBranch: optional.Some(false),
+		ListOptions:     db.ListOptionsAll,
 	}
 	branches, err := git_model.FindBranchNames(ctx, branchOpts)
 	if err != nil {
@@ -720,10 +720,8 @@ func GetTagList(ctx *context.Context) {
 func PrepareBranchList(ctx *context.Context) {
 	branchOpts := git_model.FindBranchOptions{
 		RepoID:          ctx.Repo.Repository.ID,
-		IsDeletedBranch: util.OptionalBoolFalse,
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		IsDeletedBranch: optional.Some(false),
+		ListOptions:     db.ListOptionsAll,
 	}
 	brs, err := git_model.FindBranchNames(ctx, branchOpts)
 	if err != nil {
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index 3c0fa4bc00..46f0208453 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -5,31 +5,28 @@ package repo
 
 import (
 	"net/http"
+	"strings"
 
+	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/git"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 const tplSearch base.TplName = "repo/search"
 
 // Search render repository search page
 func Search(ctx *context.Context) {
-	if !setting.Indexer.RepoIndexerEnabled {
-		ctx.Redirect(ctx.Repo.RepoLink)
-		return
-	}
-
 	language := ctx.FormTrim("l")
 	keyword := ctx.FormTrim("q")
 
-	queryType := ctx.FormTrim("t")
-	isMatch := queryType == "match"
+	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
 
 	ctx.Data["Keyword"] = keyword
 	ctx.Data["Language"] = language
-	ctx.Data["queryType"] = queryType
+	ctx.Data["IsFuzzy"] = isFuzzy
 	ctx.Data["PageIsViewCode"] = true
 
 	if keyword == "" {
@@ -42,25 +39,61 @@ func Search(ctx *context.Context) {
 		page = 1
 	}
 
-	total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID},
-		language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
-	if err != nil {
-		if code_indexer.IsAvailable(ctx) {
-			ctx.ServerError("SearchResults", err)
+	var total int
+	var searchResults []*code_indexer.Result
+	var searchResultLanguages []*code_indexer.SearchResultLanguages
+	if setting.Indexer.RepoIndexerEnabled {
+		var err error
+		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
+			RepoIDs:        []int64{ctx.Repo.Repository.ID},
+			Keyword:        keyword,
+			IsKeywordFuzzy: isFuzzy,
+			Language:       language,
+			Paginator: &db.ListOptions{
+				Page:     page,
+				PageSize: setting.UI.RepoSearchPagingNum,
+			},
+		})
+		if err != nil {
+			if code_indexer.IsAvailable(ctx) {
+				ctx.ServerError("SearchResults", err)
+				return
+			}
+			ctx.Data["CodeIndexerUnavailable"] = true
+		} else {
+			ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
+		}
+	} else {
+		res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ContextLineNumber: 3, IsFuzzy: isFuzzy})
+		if err != nil {
+			ctx.ServerError("GrepSearch", err)
 			return
 		}
-		ctx.Data["CodeIndexerUnavailable"] = true
-	} else {
-		ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
+		total = len(res)
+		pageStart := min((page-1)*setting.UI.RepoSearchPagingNum, len(res))
+		pageEnd := min(page*setting.UI.RepoSearchPagingNum, len(res))
+		res = res[pageStart:pageEnd]
+		for _, r := range res {
+			searchResults = append(searchResults, &code_indexer.Result{
+				RepoID:   ctx.Repo.Repository.ID,
+				Filename: r.Filename,
+				CommitID: ctx.Repo.CommitID,
+				// UpdatedUnix: not supported yet
+				// Language:    not supported yet
+				// Color:       not supported yet
+				Lines: code_indexer.HighlightSearchResultCode(r.Filename, "", r.LineNumbers, strings.Join(r.LineCodes, "\n")),
+			})
+		}
 	}
 
-	ctx.Data["SourcePath"] = ctx.Repo.Repository.Link()
+	ctx.Data["CodeIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+	ctx.Data["Repo"] = ctx.Repo.Repository
 	ctx.Data["SearchResults"] = searchResults
 	ctx.Data["SearchResultLanguages"] = searchResultLanguages
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "l", "Language")
+	pager.AddParamString("l", language)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplSearch)
diff --git a/routers/web/repo/setting/avatar.go b/routers/web/repo/setting/avatar.go
index 02c807b775..504f57cfc2 100644
--- a/routers/web/repo/setting/avatar.go
+++ b/routers/web/repo/setting/avatar.go
@@ -8,11 +8,11 @@ import (
 	"fmt"
 	"io"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/typesniffer"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
@@ -38,7 +38,7 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
 	defer r.Close()
 
 	if form.Avatar.Size > setting.Avatar.MaxFileSize {
-		return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
+		return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
 	}
 
 	data, err := io.ReadAll(r)
@@ -47,7 +47,7 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
 	}
 	st := typesniffer.DetectContentType(data)
 	if !(st.IsImage() && !st.IsSvgImage()) {
-		return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
+		return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image"))
 	}
 	if err = repo_service.UploadAvatar(ctx, ctxRepo, data); err != nil {
 		return fmt.Errorf("UploadAvatar: %w", err)
diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go
index c5c2a88c49..31f9f76d0f 100644
--- a/routers/web/repo/setting/collaboration.go
+++ b/routers/web/repo/setting/collaboration.go
@@ -4,19 +4,19 @@
 package setting
 
 import (
+	"errors"
 	"net/http"
 	"strings"
 
-	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/mailer"
 	org_service "code.gitea.io/gitea/services/org"
 	repo_service "code.gitea.io/gitea/services/repository"
@@ -27,7 +27,7 @@ func Collaboration(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration")
 	ctx.Data["PageIsSettingsCollaboration"] = true
 
-	users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
+	users, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: ctx.Repo.Repository.ID})
 	if err != nil {
 		ctx.ServerError("GetCollaborators", err)
 		return
@@ -101,7 +101,12 @@ func CollaborationPost(ctx *context.Context) {
 	}
 
 	if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil {
-		ctx.ServerError("AddCollaborator", err)
+		if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator.blocked_user"))
+			ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+		} else {
+			ctx.ServerError("AddCollaborator", err)
+		}
 		return
 	}
 
@@ -126,10 +131,19 @@ func ChangeCollaborationAccessMode(ctx *context.Context) {
 
 // DeleteCollaboration delete a collaboration for a repository
 func DeleteCollaboration(ctx *context.Context) {
-	if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, ctx.FormInt64("id")); err != nil {
-		ctx.Flash.Error("DeleteCollaboration: " + err.Error())
+	if collaborator, err := user_model.GetUserByID(ctx, ctx.FormInt64("id")); err != nil {
+		if user_model.IsErrUserNotExist(err) {
+			ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
+		} else {
+			ctx.ServerError("GetUserByName", err)
+			return
+		}
 	} else {
-		ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
+		if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
+			ctx.Flash.Error("DeleteCollaboration: " + err.Error())
+		} else {
+			ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
+		}
 	}
 
 	ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration")
diff --git a/routers/web/repo/setting/default_branch.go b/routers/web/repo/setting/default_branch.go
index c8a576e576..881d148afc 100644
--- a/routers/web/repo/setting/default_branch.go
+++ b/routers/web/repo/setting/default_branch.go
@@ -7,10 +7,10 @@ import (
 	"net/http"
 
 	git_model "code.gitea.io/gitea/models/git"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/routers/web/repo"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/web/repo/setting/deploy_key.go b/routers/web/repo/setting/deploy_key.go
index 3d4420006c..abc3eb4af1 100644
--- a/routers/web/repo/setting/deploy_key.go
+++ b/routers/web/repo/setting/deploy_key.go
@@ -8,11 +8,11 @@ import (
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/repo/setting/git_hooks.go b/routers/web/repo/setting/git_hooks.go
index 551327d44b..217a01c90c 100644
--- a/routers/web/repo/setting/git_hooks.go
+++ b/routers/web/repo/setting/git_hooks.go
@@ -6,8 +6,8 @@ package setting
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/services/context"
 )
 
 // GitHooks hooks of a repository
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index cd0f11d548..6dddade066 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -18,7 +18,6 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/git/pipeline"
 	"code.gitea.io/gitea/modules/lfs"
@@ -28,6 +27,7 @@ import (
 	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/modules/typesniffer"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -287,22 +287,19 @@ func LFSFileGet(ctx *context.Context) {
 
 	st := typesniffer.DetectContentType(buf)
 	ctx.Data["IsTextFile"] = st.IsText()
-	isRepresentableAsText := st.IsRepresentableAsText()
-
-	fileSize := meta.Size
 	ctx.Data["FileSize"] = meta.Size
 	ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct")
 	switch {
-	case isRepresentableAsText:
-		if st.IsSvgImage() {
-			ctx.Data["IsImageFile"] = true
-		}
-
-		if fileSize >= setting.UI.MaxDisplayFileSize {
+	case st.IsRepresentableAsText():
+		if meta.Size >= setting.UI.MaxDisplayFileSize {
 			ctx.Data["IsFileTooLarge"] = true
 			break
 		}
 
+		if st.IsSvgImage() {
+			ctx.Data["IsImageFile"] = true
+		}
+
 		rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
 
 		// Building code view blocks with line number on server side.
@@ -338,6 +335,8 @@ func LFSFileGet(ctx *context.Context) {
 		ctx.Data["IsAudioFile"] = true
 	case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
 		ctx.Data["IsImageFile"] = true
+	default:
+		// TODO: the logic is not the same as "renderFile" in "view.go"
 	}
 	ctx.HTML(http.StatusOK, tplSettingsLFSFile)
 }
@@ -388,7 +387,7 @@ func LFSFileFind(ctx *context.Context) {
 	sha := ctx.FormString("sha")
 	ctx.Data["Title"] = oid
 	ctx.Data["PageIsSettingsLFS"] = true
-	objectFormat, _ := ctx.Repo.GitRepo.GetObjectFormat()
+	objectFormat := ctx.Repo.GetObjectFormat()
 	var objectID git.ObjectID
 	if len(sha) == 0 {
 		pointer := lfs.Pointer{Oid: oid, Size: size}
diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go
index 98d6977b81..4bab3f897a 100644
--- a/routers/web/repo/setting/protected_branch.go
+++ b/routers/web/repo/setting/protected_branch.go
@@ -15,9 +15,9 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/web/repo"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	pull_service "code.gitea.io/gitea/services/pull"
 	"code.gitea.io/gitea/services/repository"
@@ -68,7 +68,7 @@ func SettingsProtectedBranch(c *context.Context) {
 	}
 
 	c.Data["PageIsSettingsBranches"] = true
-	c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + rule.RuleName
+	c.Data["Title"] = c.Locale.TrString("repo.settings.protected_branch") + " - " + rule.RuleName
 
 	users, err := access_model.GetRepoReaders(c, c.Repo.Repository)
 	if err != nil {
@@ -313,7 +313,13 @@ func RenameBranchPost(ctx *context.Context) {
 
 	msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To)
 	if err != nil {
-		ctx.ServerError("RenameBranch", err)
+		switch {
+		case git_model.IsErrBranchAlreadyExists(err):
+			ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To))
+			ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+		default:
+			ctx.ServerError("RenameBranch", err)
+		}
 		return
 	}
 
diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go
index 46addb3f0a..2c25b650b9 100644
--- a/routers/web/repo/setting/protected_tag.go
+++ b/routers/web/repo/setting/protected_tag.go
@@ -13,9 +13,9 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/repo/setting/runners.go b/routers/web/repo/setting/runners.go
index 8d4112c157..a47d3b45e2 100644
--- a/routers/web/repo/setting/runners.go
+++ b/routers/web/repo/setting/runners.go
@@ -11,10 +11,10 @@ import (
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	actions_shared "code.gitea.io/gitea/routers/web/shared/actions"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go
index cf427b2c44..d4d56bfc57 100644
--- a/routers/web/repo/setting/secrets.go
+++ b/routers/web/repo/setting/secrets.go
@@ -8,10 +8,10 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	shared "code.gitea.io/gitea/routers/web/shared/secrets"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 8c1daf52bc..00a5282f34 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -5,6 +5,7 @@
 package setting
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -12,25 +13,26 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
+	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/indexer/stats"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
-	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web"
+	actions_service "code.gitea.io/gitea/services/actions"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/migrations"
 	mirror_service "code.gitea.io/gitea/services/mirror"
@@ -488,6 +490,13 @@ func SettingsPost(ctx *context.Context) {
 			}
 		}
 
+		if form.DefaultWikiBranch != "" {
+			if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil {
+				log.Error("ChangeDefaultWikiBranch failed, err: %v", err)
+				ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch"))
+			}
+		}
+
 		if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
 			if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
 				ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
@@ -534,6 +543,9 @@ func SettingsPost(ctx *context.Context) {
 			units = append(units, repo_model.RepoUnit{
 				RepoID: repo.ID,
 				Type:   unit_model.TypeProjects,
+				Config: &repo_model.ProjectsConfig{
+					ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
+				},
 			})
 		} else if !unit_model.TypeProjects.UnitGlobalDisabled() {
 			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
@@ -576,6 +588,7 @@ func SettingsPost(ctx *context.Context) {
 					AllowRebase:                   form.PullsAllowRebase,
 					AllowRebaseMerge:              form.PullsAllowRebaseMerge,
 					AllowSquash:                   form.PullsAllowSquash,
+					AllowFastForwardOnly:          form.PullsAllowFastForwardOnly,
 					AllowManualMerge:              form.PullsAllowManualMerge,
 					AutodetectManualMerge:         form.EnableAutodetectManualMerge,
 					AllowRebaseUpdate:             form.PullsAllowRebaseUpdate,
@@ -692,7 +705,7 @@ func SettingsPost(ctx *context.Context) {
 		}
 		repo.IsMirror = false
 
-		if _, err := repo_module.CleanUpMigrateInfo(ctx, repo); err != nil {
+		if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil {
 			ctx.ServerError("CleanUpMigrateInfo", err)
 			return
 		} else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil {
@@ -779,6 +792,8 @@ func SettingsPost(ctx *context.Context) {
 				ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
 			} else if models.IsErrRepoTransferInProgress(err) {
 				ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
+			} else if errors.Is(err, user_model.ErrBlockedUser) {
+				ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
 			} else {
 				ctx.ServerError("TransferOwnership", err)
 			}
@@ -884,6 +899,10 @@ func SettingsPost(ctx *context.Context) {
 			return
 		}
 
+		if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
+			log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+		}
+
 		ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
 
 		log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
@@ -902,6 +921,12 @@ func SettingsPost(ctx *context.Context) {
 			return
 		}
 
+		if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
+			if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
+				log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+			}
+		}
+
 		ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
 
 		log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go
index 066d2ef2a9..09586cc68d 100644
--- a/routers/web/repo/setting/settings_test.go
+++ b/routers/web/repo/setting/settings_test.go
@@ -14,10 +14,10 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/forms"
 	repo_service "code.gitea.io/gitea/services/repository"
 
diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go
index 428aa0bd5c..45b6c0f39a 100644
--- a/routers/web/repo/setting/variables.go
+++ b/routers/web/repo/setting/variables.go
@@ -8,10 +8,10 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	shared "code.gitea.io/gitea/routers/web/shared/actions"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index f9f41d0bd5..1a3549fea4 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -18,15 +18,14 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	webhook_service "code.gitea.io/gitea/services/webhook"
@@ -152,6 +151,7 @@ func WebhooksNew(ctx *context.Context) {
 		}
 	}
 	ctx.Data["BaseLink"] = orCtx.LinkNew
+	ctx.Data["BaseLinkNew"] = orCtx.LinkNew
 
 	ctx.HTML(http.StatusOK, orCtx.NewTemplate)
 }
@@ -588,6 +588,7 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) {
 		return nil, nil
 	}
 	ctx.Data["BaseLink"] = orCtx.Link
+	ctx.Data["BaseLinkNew"] = orCtx.LinkNew
 
 	var w *webhook.Webhook
 	if orCtx.RepoID > 0 {
@@ -656,12 +657,7 @@ func TestWebhook(ctx *context.Context) {
 	commit := ctx.Repo.Commit
 	if commit == nil {
 		ghost := user_model.NewGhostUser()
-		objectFormat, err := gitrepo.GetObjectFormatOfRepo(ctx, ctx.Repo.Repository)
-		if err != nil {
-			ctx.Flash.Error("GetObjectFormatOfRepo: " + err.Error())
-			ctx.Status(http.StatusInternalServerError)
-			return
-		}
+		objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName)
 		commit = &git.Commit{
 			ID:            objectFormat.EmptyObjectID(),
 			Author:        ghost.NewGitSig(),
diff --git a/routers/web/repo/topic.go b/routers/web/repo/topic.go
index d0e706c5bd..d81a695df9 100644
--- a/routers/web/repo/topic.go
+++ b/routers/web/repo/topic.go
@@ -8,8 +8,8 @@ import (
 	"strings"
 
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 // TopicsPost response for creating repository
diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go
index c364e7090f..d11af4669f 100644
--- a/routers/web/repo/treelist.go
+++ b/routers/web/repo/treelist.go
@@ -7,8 +7,8 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/go-enry/go-enry/v2"
 )
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index af3021da11..8aa9dbb1be 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -35,8 +35,6 @@ import (
 	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/highlight"
 	"code.gitea.io/gitea/modules/lfs"
@@ -45,10 +43,13 @@ import (
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/svg"
 	"code.gitea.io/gitea/modules/typesniffer"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/feed"
+	"code.gitea.io/gitea/services/context"
 	issue_service "code.gitea.io/gitea/services/issue"
+	files_service "code.gitea.io/gitea/services/repository/files"
 
 	"github.com/nektos/act/pkg/model"
 
@@ -358,7 +359,7 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
 		ctx.Data["LatestCommitVerification"] = verification
 		ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
 
-		statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptions{ListAll: true})
+		statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
 		if err != nil {
 			log.Error("GetLatestCommitStatus: %v", err)
 		}
@@ -481,17 +482,17 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 
 	switch {
 	case isRepresentableAsText:
+		if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
+			ctx.Data["IsFileTooLarge"] = true
+			break
+		}
+
 		if fInfo.st.IsSvgImage() {
 			ctx.Data["IsImageFile"] = true
 			ctx.Data["CanCopyContent"] = true
 			ctx.Data["HasSourceRenderedToggle"] = true
 		}
 
-		if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
-			ctx.Data["IsFileTooLarge"] = true
-			break
-		}
-
 		rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
 
 		shouldRenderSource := ctx.FormString("display") == "source"
@@ -553,31 +554,11 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 			}
 			ctx.Data["NumLinesSet"] = true
 
-			language := ""
-
-			indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
-			if err == nil {
-				defer deleteTemporaryFile()
-
-				filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
-					CachedOnly: true,
-					Attributes: []string{"linguist-language", "gitlab-language"},
-					Filenames:  []string{ctx.Repo.TreePath},
-					IndexFile:  indexFilename,
-					WorkTree:   worktree,
-				})
-				if err != nil {
-					log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
-				}
-
-				language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
-				if language == "" || language == "unspecified" {
-					language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
-				}
-				if language == "unspecified" {
-					language = ""
-				}
+			language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
+			if err != nil {
+				log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
 			}
+
 			fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
 			ctx.Data["LexerName"] = lexerName
 			if err != nil {
@@ -625,6 +606,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 			break
 		}
 
+		// TODO: this logic seems strange, it duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
+		// maybe for this case, the file is a binary file, and shouldn't be rendered?
 		if markupType := markup.Type(blob.Name()); markupType != "" {
 			rd := io.MultiReader(bytes.NewReader(buf), dataRc)
 			ctx.Data["IsMarkup"] = true
@@ -653,11 +636,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 			defer deferable()
 			attrs, err := checker.CheckPath(ctx.Repo.TreePath)
 			if err == nil {
-				vendored, has := attrs["linguist-vendored"]
-				ctx.Data["IsVendored"] = has && (vendored == "set" || vendored == "true")
-
-				generated, has := attrs["linguist-generated"]
-				ctx.Data["IsGenerated"] = has && (generated == "set" || generated == "true")
+				ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value()
+				ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value()
 			}
 		}
 	}
@@ -758,7 +738,7 @@ func checkHomeCodeViewable(ctx *context.Context) {
 		}
 	}
 
-	ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo")))
+	ctx.NotFound("Home", fmt.Errorf(ctx.Locale.TrString("units.error.no_unit_allowed_repo")))
 }
 
 func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
@@ -811,7 +791,7 @@ func Home(ctx *context.Context) {
 		return
 	}
 
-	renderCode(ctx)
+	renderHomeCode(ctx)
 }
 
 // LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
@@ -880,25 +860,18 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
 		defer cancel()
 	}
 
-	selected := make(container.Set[string])
-	selected.AddMultiple(ctx.FormStrings("f[]")...)
-
-	entries := allEntries
-	if len(selected) > 0 {
-		entries = make(git.Entries, 0, len(selected))
-		for _, entry := range allEntries {
-			if selected.Contains(entry.Name()) {
-				entries = append(entries, entry)
-			}
-		}
-	}
-
-	var latestCommit *git.Commit
-	ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
+	files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
 	if err != nil {
 		ctx.ServerError("GetCommitsInfo", err)
 		return nil
 	}
+	ctx.Data["Files"] = files
+	for _, f := range files {
+		if f.Commit == nil {
+			ctx.Data["HasFilesWithoutLatestCommit"] = true
+			break
+		}
+	}
 
 	if !loadLatestCommitData(ctx, latestCommit) {
 		return nil
@@ -928,7 +901,7 @@ func renderLanguageStats(ctx *context.Context) {
 }
 
 func renderRepoTopics(ctx *context.Context) {
-	topics, _, err := repo_model.FindTopics(ctx, &repo_model.FindTopicOptions{
+	topics, err := db.Find[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
 		RepoID: ctx.Repo.Repository.ID,
 	})
 	if err != nil {
@@ -938,9 +911,33 @@ func renderRepoTopics(ctx *context.Context) {
 	ctx.Data["Topics"] = topics
 }
 
-func renderCode(ctx *context.Context) {
+func prepareOpenWithEditorApps(ctx *context.Context) {
+	var tmplApps []map[string]any
+	apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx)
+	if len(apps) == 0 {
+		apps = setting.DefaultOpenWithEditorApps()
+	}
+	for _, app := range apps {
+		schema, _, _ := strings.Cut(app.OpenURL, ":")
+		var iconHTML template.HTML
+		if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
+			iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16, "tw-mr-2")
+		} else {
+			iconHTML = svg.RenderHTML("gitea-git", 16, "tw-mr-2") // TODO: it could support user's customized icon in the future
+		}
+		tmplApps = append(tmplApps, map[string]any{
+			"DisplayName": app.DisplayName,
+			"OpenURL":     app.OpenURL,
+			"IconHTML":    iconHTML,
+		})
+	}
+	ctx.Data["OpenWithEditorApps"] = tmplApps
+}
+
+func renderHomeCode(ctx *context.Context) {
 	ctx.Data["PageIsViewCode"] = true
 	ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
+	prepareOpenWithEditorApps(ctx)
 
 	if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
 		showEmpty := true
@@ -1003,6 +1000,8 @@ func renderCode(ctx *context.Context) {
 		return
 	}
 
+	checkOutdatedBranch(ctx)
+
 	checkCitationFile(ctx, entry)
 	if ctx.Written() {
 		return
@@ -1069,6 +1068,31 @@ func renderCode(ctx *context.Context) {
 	ctx.HTML(http.StatusOK, tplRepoHome)
 }
 
+func checkOutdatedBranch(ctx *context.Context) {
+	if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) {
+		return
+	}
+
+	// get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName`
+	commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
+	if err != nil {
+		log.Error("GetBranchCommitID: %v", err)
+		// Don't return an error page, as it can be rechecked the next time the user opens the page.
+		return
+	}
+
+	dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName)
+	if err != nil {
+		log.Error("GetBranch: %v", err)
+		// Don't return an error page, as it can be rechecked the next time the user opens the page.
+		return
+	}
+
+	if dbBranch.CommitID != commit.ID.String() {
+		ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true)
+	}
+}
+
 // RenderUserCards render a page show users according the input template
 func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) {
 	page := ctx.FormInt("page")
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index 5e7b971e67..df15f61b17 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -18,7 +18,6 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
@@ -29,6 +28,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/common"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	notify_service "code.gitea.io/gitea/services/notify"
 	wiki_service "code.gitea.io/gitea/services/wiki"
@@ -93,17 +93,32 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
 }
 
 func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
-	wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
-	if err != nil {
-		ctx.ServerError("OpenRepository", err)
-		return nil, nil, err
+	wikiGitRepo, errGitRepo := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
+	if errGitRepo != nil {
+		ctx.ServerError("OpenRepository", errGitRepo)
+		return nil, nil, errGitRepo
 	}
 
-	commit, err := wikiRepo.GetBranchCommit(wiki_service.DefaultBranch)
-	if err != nil {
-		return wikiRepo, nil, err
+	commit, errCommit := wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
+	if git.IsErrNotExist(errCommit) {
+		// if the default branch recorded in database is out of sync, then re-sync it
+		gitRepoDefaultBranch, errBranch := gitrepo.GetWikiDefaultBranch(ctx, ctx.Repo.Repository)
+		if errBranch != nil {
+			return wikiGitRepo, nil, errBranch
+		}
+		// update the default branch in the database
+		errDb := repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, DefaultWikiBranch: gitRepoDefaultBranch}, "default_wiki_branch")
+		if errDb != nil {
+			return wikiGitRepo, nil, errDb
+		}
+		ctx.Repo.Repository.DefaultWikiBranch = gitRepoDefaultBranch
+		// retry to get the commit from the correct default branch
+		commit, errCommit = wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
 	}
-	return wikiRepo, commit, nil
+	if errCommit != nil {
+		return wikiGitRepo, nil, errCommit
+	}
+	return wikiGitRepo, commit, nil
 }
 
 // wikiContentsByEntry returns the contents of the wiki page referenced by the
@@ -316,7 +331,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
 	}
 
 	// get commit count - wiki revisions
-	commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename)
+	commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
 	ctx.Data["CommitCount"] = commitsCount
 
 	return wikiRepo, entry
@@ -368,7 +383,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
 	ctx.Data["footerContent"] = ""
 
 	// get commit count - wiki revisions
-	commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename)
+	commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
 	ctx.Data["CommitCount"] = commitsCount
 
 	// get page
@@ -380,7 +395,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
 	// get Commit Count
 	commitsHistory, err := wikiRepo.CommitsByFileAndRange(
 		git.CommitsByFileAndRangeOptions{
-			Revision: wiki_service.DefaultBranch,
+			Revision: ctx.Repo.Repository.DefaultWikiBranch,
 			File:     pageFilename,
 			Page:     page,
 		})
@@ -402,20 +417,17 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
 
 func renderEditPage(ctx *context.Context) {
 	wikiRepo, commit, err := findWikiRepoCommit(ctx)
-	if err != nil {
+	defer func() {
 		if wikiRepo != nil {
-			wikiRepo.Close()
+			_ = wikiRepo.Close()
 		}
+	}()
+	if err != nil {
 		if !git.IsErrNotExist(err) {
 			ctx.ServerError("GetBranchCommit", err)
 		}
 		return
 	}
-	defer func() {
-		if wikiRepo != nil {
-			wikiRepo.Close()
-		}
-	}()
 
 	// get requested pagename
 	pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
@@ -584,17 +596,15 @@ func WikiPages(ctx *context.Context) {
 	ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
 
 	wikiRepo, commit, err := findWikiRepoCommit(ctx)
-	if err != nil {
-		if wikiRepo != nil {
-			wikiRepo.Close()
-		}
-		return
-	}
 	defer func() {
 		if wikiRepo != nil {
-			wikiRepo.Close()
+			_ = wikiRepo.Close()
 		}
 	}()
+	if err != nil {
+		ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
+		return
+	}
 
 	entries, err := commit.ListEntries()
 	if err != nil {
@@ -714,7 +724,7 @@ func NewWikiPost(ctx *context.Context) {
 	wikiName := wiki_service.UserTitleToWebPath("", form.Title)
 
 	if len(form.Message) == 0 {
-		form.Message = ctx.Tr("repo.editor.add", form.Title)
+		form.Message = ctx.Locale.TrString("repo.editor.add", form.Title)
 	}
 
 	if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil {
@@ -766,7 +776,7 @@ func EditWikiPost(ctx *context.Context) {
 	newWikiName := wiki_service.UserTitleToWebPath("", form.Title)
 
 	if len(form.Message) == 0 {
-		form.Message = ctx.Tr("repo.editor.update", form.Title)
+		form.Message = ctx.Locale.TrString("repo.editor.update", form.Title)
 	}
 
 	if err := wiki_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.Content, form.Message); err != nil {
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index d3decdae2d..4602dcfeb4 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -9,12 +9,13 @@ import (
 	"net/url"
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/forms"
 	wiki_service "code.gitea.io/gitea/services/wiki"
 
@@ -79,7 +80,7 @@ func assertPagesMetas(t *testing.T, expectedNames []string, metas any) {
 func TestWiki(t *testing.T) {
 	unittest.PrepareTestEnv(t)
 
-	ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages")
+	ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki")
 	ctx.SetParams("*", "Home")
 	contexttest.LoadRepo(t, ctx, 1)
 	Wiki(ctx)
@@ -144,7 +145,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) {
 	})
 	NewWikiPost(ctx)
 	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-	assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page"), ctx.Flash.ErrorMsg)
+	assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg)
 	assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
 }
 
@@ -199,12 +200,13 @@ func TestDeleteWikiPagePost(t *testing.T) {
 
 func TestWikiRaw(t *testing.T) {
 	for filepath, filetype := range map[string]string{
-		"jpeg.jpg":                 "image/jpeg",
-		"images/jpeg.jpg":          "image/jpeg",
-		"Page With Spaced Name":    "text/plain; charset=utf-8",
-		"Page-With-Spaced-Name":    "text/plain; charset=utf-8",
-		"Page With Spaced Name.md": "", // there is no "Page With Spaced Name.md" in repo
-		"Page-With-Spaced-Name.md": "text/plain; charset=utf-8",
+		"jpeg.jpg":                      "image/jpeg",
+		"images/jpeg.jpg":               "image/jpeg",
+		"files/Non-Renderable-File.zip": "application/octet-stream",
+		"Page With Spaced Name":         "text/plain; charset=utf-8",
+		"Page-With-Spaced-Name":         "text/plain; charset=utf-8",
+		"Page With Spaced Name.md":      "", // there is no "Page With Spaced Name.md" in repo
+		"Page-With-Spaced-Name.md":      "text/plain; charset=utf-8",
 	} {
 		unittest.PrepareTestEnv(t)
 
@@ -221,3 +223,38 @@ func TestWikiRaw(t *testing.T) {
 		}
 	}
 }
+
+func TestDefaultWikiBranch(t *testing.T) {
+	unittest.PrepareTestEnv(t)
+
+	// repo with no wiki
+	repoWithNoWiki := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+	assert.False(t, repoWithNoWiki.HasWiki())
+	assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repoWithNoWiki, "main"))
+
+	// repo with wiki
+	assert.NoError(t, repo_model.UpdateRepositoryCols(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"}))
+
+	ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki")
+	ctx.SetParams("*", "Home")
+	contexttest.LoadRepo(t, ctx, 1)
+	assert.Equal(t, "wrong-branch", ctx.Repo.Repository.DefaultWikiBranch)
+	Wiki(ctx) // after the visiting, the out-of-sync database record will update the branch name to "master"
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	assert.Equal(t, "master", ctx.Repo.Repository.DefaultWikiBranch)
+
+	// invalid branch name should fail
+	assert.Error(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repo, "the bad name"))
+	repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	assert.Equal(t, "master", repo.DefaultWikiBranch)
+
+	// the same branch name, should succeed (actually a no-op)
+	assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repo, "master"))
+	repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	assert.Equal(t, "master", repo.DefaultWikiBranch)
+
+	// change to another name
+	assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repo, "main"))
+	repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	assert.Equal(t, "main", repo.DefaultWikiBranch)
+}
diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go
index ae9a376724..34b7969442 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -8,10 +8,10 @@ import (
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go
index 07a0575207..79c03e4e8c 100644
--- a/routers/web/shared/actions/variables.go
+++ b/routers/web/shared/actions/variables.go
@@ -4,17 +4,13 @@
 package actions
 
 import (
-	"errors"
-	"regexp"
-	"strings"
-
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/web"
+	actions_service "code.gitea.io/gitea/services/actions"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
-	secret_service "code.gitea.io/gitea/services/secrets"
 )
 
 func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
@@ -29,41 +25,16 @@ func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
 	ctx.Data["Variables"] = variables
 }
 
-// some regular expression of `variables` and `secrets`
-// reference to:
-// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
-// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
-var (
-	forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
-)
-
-func envNameCIRegexMatch(name string) error {
-	if forbiddenEnvNameCIRx.MatchString(name) {
-		log.Error("Env Name cannot be ci")
-		return errors.New("env name cannot be ci")
-	}
-	return nil
-}
-
 func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
 	form := web.GetForm(ctx).(*forms.EditVariableForm)
 
-	if err := secret_service.ValidateName(form.Name); err != nil {
-		ctx.JSONError(err.Error())
-		return
-	}
-
-	if err := envNameCIRegexMatch(form.Name); err != nil {
-		ctx.JSONError(err.Error())
-		return
-	}
-
-	v, err := actions_model.InsertVariable(ctx, ownerID, repoID, form.Name, ReserveLineBreakForTextarea(form.Data))
+	v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data)
 	if err != nil {
-		log.Error("InsertVariable error: %v", err)
+		log.Error("CreateVariable: %v", err)
 		ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
 		return
 	}
+
 	ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
 	ctx.JSONRedirect(redirectURL)
 }
@@ -72,23 +43,8 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
 	id := ctx.ParamsInt64(":variable_id")
 	form := web.GetForm(ctx).(*forms.EditVariableForm)
 
-	if err := secret_service.ValidateName(form.Name); err != nil {
-		ctx.JSONError(err.Error())
-		return
-	}
-
-	if err := envNameCIRegexMatch(form.Name); err != nil {
-		ctx.JSONError(err.Error())
-		return
-	}
-
-	ok, err := actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
-		ID:   id,
-		Name: strings.ToUpper(form.Name),
-		Data: ReserveLineBreakForTextarea(form.Data),
-	})
-	if err != nil || !ok {
-		log.Error("UpdateVariable error: %v", err)
+	if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok {
+		log.Error("UpdateVariable: %v", err)
 		ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
 		return
 	}
@@ -99,7 +55,7 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
 func DeleteVariable(ctx *context.Context, redirectURL string) {
 	id := ctx.ParamsInt64(":variable_id")
 
-	if _, err := db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: id}); err != nil {
+	if err := actions_service.DeleteVariableByID(ctx, id); err != nil {
 		log.Error("Delete variable [%d] failed: %v", id, err)
 		ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
 		return
@@ -107,12 +63,3 @@ func DeleteVariable(ctx *context.Context, redirectURL string) {
 	ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
 	ctx.JSONRedirect(redirectURL)
 }
-
-func ReserveLineBreakForTextarea(input string) string {
-	// Since the content is from a form which is a textarea, the line endings are \r\n.
-	// It's a standard behavior of HTML.
-	// But we want to store them as \n like what GitHub does.
-	// And users are unlikely to really need to keep the \r.
-	// Other than this, we should respect the original content, even leading or trailing spaces.
-	return strings.ReplaceAll(input, "\r\n", "\n")
-}
diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go
index 30c25374d1..57671ad8f1 100644
--- a/routers/web/shared/packages/packages.go
+++ b/routers/web/shared/packages/packages.go
@@ -12,10 +12,10 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	cargo_service "code.gitea.io/gitea/services/packages/cargo"
 	container_service "code.gitea.io/gitea/services/packages/container"
@@ -157,7 +157,7 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) {
 	for _, p := range packages {
 		pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 			PackageID:  p.ID,
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 			Sort:       packages_model.SortCreatedDesc,
 			Paginator:  db.NewAbsoluteListOptions(pcr.KeepCount, 200),
 		})
diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go
index c805da734a..3bd421f86a 100644
--- a/routers/web/shared/secrets/secrets.go
+++ b/routers/web/shared/secrets/secrets.go
@@ -6,10 +6,10 @@ package secrets
 import (
 	"code.gitea.io/gitea/models/db"
 	secret_model "code.gitea.io/gitea/models/secret"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
-	"code.gitea.io/gitea/routers/web/shared/actions"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	secret_service "code.gitea.io/gitea/services/secrets"
 )
@@ -27,7 +27,7 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
 func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
 	form := web.GetForm(ctx).(*forms.AddSecretForm)
 
-	s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, actions.ReserveLineBreakForTextarea(form.Data))
+	s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data))
 	if err != nil {
 		log.Error("CreateOrUpdateSecret failed: %v", err)
 		ctx.JSONError(ctx.Tr("secrets.creation.failed"))
diff --git a/routers/web/shared/user/block.go b/routers/web/shared/user/block.go
new file mode 100644
index 0000000000..8a2357623f
--- /dev/null
+++ b/routers/web/shared/user/block.go
@@ -0,0 +1,76 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"errors"
+
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/forms"
+	user_service "code.gitea.io/gitea/services/user"
+)
+
+func BlockedUsers(ctx *context.Context, blocker *user_model.User) {
+	blocks, _, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
+		BlockerID: blocker.ID,
+	})
+	if err != nil {
+		ctx.ServerError("FindBlockings", err)
+		return
+	}
+	if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
+		ctx.ServerError("LoadAttributes", err)
+		return
+	}
+	ctx.Data["UserBlocks"] = blocks
+}
+
+func BlockedUsersPost(ctx *context.Context, blocker *user_model.User) {
+	form := web.GetForm(ctx).(*forms.BlockUserForm)
+	if ctx.HasError() {
+		ctx.ServerError("FormValidation", nil)
+		return
+	}
+
+	blockee, err := user_model.GetUserByName(ctx, form.Blockee)
+	if err != nil {
+		ctx.ServerError("GetUserByName", nil)
+		return
+	}
+
+	switch form.Action {
+	case "block":
+		if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note); err != nil {
+			if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
+				ctx.Flash.Error(ctx.Tr("user.block.block.failure", err.Error()))
+			} else {
+				ctx.ServerError("BlockUser", err)
+				return
+			}
+		}
+	case "unblock":
+		if err := user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee); err != nil {
+			if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
+				ctx.Flash.Error(ctx.Tr("user.block.unblock.failure", err.Error()))
+			} else {
+				ctx.ServerError("UnblockUser", err)
+				return
+			}
+		}
+	case "note":
+		block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
+		if err != nil {
+			ctx.ServerError("GetBlocking", err)
+			return
+		}
+		if block != nil {
+			if err := user_model.UpdateBlockingNote(ctx, block.ID, form.Note); err != nil {
+				ctx.ServerError("UpdateBlockingNote", err)
+				return
+			}
+		}
+	}
+}
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index a2c0abb47e..7531e1ba26 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -4,6 +4,8 @@
 package user
 
 import (
+	"net/url"
+
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
@@ -11,14 +13,14 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 )
 
 // prepareContextForCommonProfile store some common data into context data for user's profile related pages (including the nav menu)
@@ -36,8 +38,9 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 
 	ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 	ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
-	ctx.Data["UserLocationMapURL"] = setting.Service.UserLocationMapURL
-
+	if setting.Service.UserLocationMapURL != "" {
+		ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location)
+	}
 	// Show OpenID URIs
 	openIDs, err := user_model.GetUserOpenIDs(ctx, ctx.ContextUser.ID)
 	if err != nil {
@@ -45,7 +48,6 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 		return
 	}
 	ctx.Data["OpenIDs"] = openIDs
-
 	if len(ctx.ContextUser.Description) != 0 {
 		content, err := markdown.RenderString(&markup.RenderContext{
 			Metas: map[string]string{"mode": "document"},
@@ -84,6 +86,14 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 	if _, ok := ctx.Data["NumFollowing"]; !ok {
 		_, ctx.Data["NumFollowing"], _ = user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{PageSize: 1, Page: 1})
 	}
+
+	if ctx.Doer != nil {
+		if block, err := user_model.GetBlocking(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
+			ctx.ServerError("GetBlocking", err)
+		} else {
+			ctx.Data["UserBlocking"] = block
+		}
+	}
 }
 
 func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
@@ -126,7 +136,7 @@ func LoadHeaderCount(ctx *context.Context) error {
 		Actor:              ctx.Doer,
 		OwnerID:            ctx.ContextUser.ID,
 		Private:            ctx.IsSigned,
-		Collaborate:        util.OptionalBoolFalse,
+		Collaborate:        optional.Some(false),
 		IncludeDescription: setting.UI.SearchRepoDescription,
 	})
 	if err != nil {
@@ -142,7 +152,7 @@ func LoadHeaderCount(ctx *context.Context) error {
 	}
 	projectCount, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
 		OwnerID:  ctx.ContextUser.ID,
-		IsClosed: util.OptionalBoolOf(false),
+		IsClosed: optional.Some(false),
 		Type:     projectType,
 	})
 	if err != nil {
diff --git a/routers/web/swagger_json.go b/routers/web/swagger_json.go
index 493c97aa67..fc39b504a9 100644
--- a/routers/web/swagger_json.go
+++ b/routers/web/swagger_json.go
@@ -4,22 +4,10 @@
 package web
 
 import (
-	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 )
 
-// tplSwaggerV1Json swagger v1 json template
-const tplSwaggerV1Json base.TplName = "swagger/v1_json"
-
 // SwaggerV1Json render swagger v1 json
 func SwaggerV1Json(ctx *context.Context) {
-	t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json), nil)
-	if err != nil {
-		ctx.ServerError("unable to find template", err)
-		return
-	}
-	ctx.Resp.Header().Set("Content-Type", "application/json")
-	if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
-		ctx.ServerError("unable to execute template", err)
-	}
+	ctx.JSONTemplate("swagger/v1_json")
 }
diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go
index 772cc38bea..04f510161d 100644
--- a/routers/web/user/avatar.go
+++ b/routers/web/user/avatar.go
@@ -9,8 +9,8 @@ import (
 
 	"code.gitea.io/gitea/models/avatars"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/httpcache"
+	"code.gitea.io/gitea/services/context"
 )
 
 func cacheableRedirect(ctx *context.Context, location string) {
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index ee514a7cfe..785c37b124 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -6,12 +6,13 @@ package user
 import (
 	"net/http"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/setting"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -39,12 +40,11 @@ func CodeSearch(ctx *context.Context) {
 	language := ctx.FormTrim("l")
 	keyword := ctx.FormTrim("q")
 
-	queryType := ctx.FormTrim("t")
-	isMatch := queryType == "match"
+	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
 
 	ctx.Data["Keyword"] = keyword
 	ctx.Data["Language"] = language
-	ctx.Data["queryType"] = queryType
+	ctx.Data["IsFuzzy"] = isFuzzy
 	ctx.Data["IsCodePage"] = true
 
 	if keyword == "" {
@@ -75,7 +75,16 @@ func CodeSearch(ctx *context.Context) {
 	)
 
 	if len(repoIDs) > 0 {
-		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
+			RepoIDs:        repoIDs,
+			Keyword:        keyword,
+			IsKeywordFuzzy: isFuzzy,
+			Language:       language,
+			Paginator: &db.ListOptions{
+				Page:     page,
+				PageSize: setting.UI.RepoSearchPagingNum,
+			},
+		})
 		if err != nil {
 			if code_indexer.IsAvailable(ctx) {
 				ctx.ServerError("SearchResults", err)
@@ -113,7 +122,7 @@ func CodeSearch(ctx *context.Context) {
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "l", "Language")
+	pager.AddParamString("l", language)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplUserCode)
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 44920817c9..ff6c2a6c36 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -24,15 +24,14 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/feed"
-	context_service "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context"
 	issue_service "code.gitea.io/gitea/services/issue"
 	pull_service "code.gitea.io/gitea/services/pull"
 
@@ -85,7 +84,7 @@ func Dashboard(ctx *context.Context) {
 		page = 1
 	}
 
-	ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard")
+	ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Locale.TrString("dashboard")
 	ctx.Data["PageIsDashboard"] = true
 	ctx.Data["PageIsNews"] = true
 	cnt, _ := organization.GetOrganizationCount(ctx, ctxUser)
@@ -134,7 +133,7 @@ func Dashboard(ctx *context.Context) {
 	ctx.Data["Feeds"] = feeds
 
 	pager := context.NewPagination(int(count), setting.UI.FeedPagingNum, page, 5)
-	pager.AddParam(ctx, "date", "Date")
+	pager.AddParamString("date", date)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplDashboard)
@@ -162,8 +161,8 @@ func Milestones(ctx *context.Context) {
 		Private:       true,
 		AllPublic:     false, // Include also all public repositories of users and public organisations
 		AllLimited:    false, // Include also all public repositories of limited organisations
-		Archived:      util.OptionalBoolFalse,
-		HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones
+		Archived:      optional.Some(false),
+		HasMilestones: optional.Some(true), // Just needs display repos has milestones
 	}
 
 	if ctxUser.IsOrganization() && ctx.Org.Team != nil {
@@ -215,7 +214,7 @@ func Milestones(ctx *context.Context) {
 	counts, err := issues_model.CountMilestonesMap(ctx, issues_model.FindMilestoneOptions{
 		RepoCond: userRepoCond,
 		Name:     keyword,
-		IsClosed: util.OptionalBoolOf(isShowClosed),
+		IsClosed: optional.Some(isShowClosed),
 	})
 	if err != nil {
 		ctx.ServerError("CountMilestonesByRepoIDs", err)
@@ -228,7 +227,7 @@ func Milestones(ctx *context.Context) {
 			PageSize: setting.UI.IssuePagingNum,
 		},
 		RepoCond: repoCond,
-		IsClosed: util.OptionalBoolOf(isShowClosed),
+		IsClosed: optional.Some(isShowClosed),
 		SortType: sortType,
 		Name:     keyword,
 	})
@@ -296,17 +295,17 @@ func Milestones(ctx *context.Context) {
 		}
 	}
 
-	showRepoIds := make(container.Set[int64], len(showRepos))
+	showRepoIDs := make(container.Set[int64], len(showRepos))
 	for _, repo := range showRepos {
 		if repo.ID > 0 {
-			showRepoIds.Add(repo.ID)
+			showRepoIDs.Add(repo.ID)
 		}
 	}
 	if len(repoIDs) == 0 {
-		repoIDs = showRepoIds.Values()
+		repoIDs = showRepoIDs.Values()
 	}
 	repoIDs = slices.DeleteFunc(repoIDs, func(v int64) bool {
-		return !showRepoIds.Contains(v)
+		return !showRepoIDs.Contains(v)
 	})
 
 	var pagerCount int
@@ -330,10 +329,10 @@ func Milestones(ctx *context.Context) {
 	ctx.Data["IsShowClosed"] = isShowClosed
 
 	pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
-	pager.AddParam(ctx, "q", "Keyword")
-	pager.AddParam(ctx, "repos", "RepoIDs")
-	pager.AddParam(ctx, "sort", "SortType")
-	pager.AddParam(ctx, "state", "State")
+	pager.AddParamString("q", keyword)
+	pager.AddParamString("repos", reposQuery)
+	pager.AddParamString("sort", sortType)
+	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplMilestones)
@@ -440,9 +439,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 
 	isPullList := unitType == unit.TypePullRequests
 	opts := &issues_model.IssuesOptions{
-		IsPull:     util.OptionalBoolOf(isPullList),
+		IsPull:     optional.Some(isPullList),
 		SortType:   sortType,
-		IsArchived: util.OptionalBoolFalse,
+		IsArchived: optional.Some(false),
 		Org:        org,
 		Team:       team,
 		User:       ctx.Doer,
@@ -466,9 +465,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 		Private:     true,
 		AllPublic:   false,
 		AllLimited:  false,
-		Collaborate: util.OptionalBoolNone,
+		Collaborate: optional.None[bool](),
 		UnitType:    unitType,
-		Archived:    util.OptionalBoolFalse,
+		Archived:    optional.Some(false),
 	}
 	if team != nil {
 		repoOpts.TeamID = team.ID
@@ -516,7 +515,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 
 	// Educated guess: Do or don't show closed issues.
 	isShowClosed := ctx.FormString("state") == "closed"
-	opts.IsClosed = util.OptionalBoolOf(isShowClosed)
+	opts.IsClosed = optional.Some(isShowClosed)
 
 	// Make sure page number is at least 1. Will be posted to ctx.Data.
 	page := ctx.FormInt("page")
@@ -530,17 +529,14 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 
 	// Get IDs for labels (a filter option for issues/pulls).
 	// Required for IssuesOptions.
-	var labelIDs []int64
 	selectedLabels := ctx.FormString("labels")
 	if len(selectedLabels) > 0 && selectedLabels != "0" {
 		var err error
-		labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
+		opts.LabelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
 		if err != nil {
-			ctx.ServerError("StringsToInt64s", err)
-			return
+			ctx.Flash.Error(ctx.Tr("invalid_data", selectedLabels), true)
 		}
 	}
-	opts.LabelIDs = labelIDs
 
 	// ------------------------------
 	// Get issues as defined by opts.
@@ -633,13 +629,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 	}
 
 	pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
-	pager.AddParam(ctx, "q", "Keyword")
-	pager.AddParam(ctx, "type", "ViewType")
-	pager.AddParam(ctx, "sort", "SortType")
-	pager.AddParam(ctx, "state", "State")
-	pager.AddParam(ctx, "labels", "SelectLabels")
-	pager.AddParam(ctx, "milestone", "MilestoneID")
-	pager.AddParam(ctx, "assignee", "AssigneeID")
+	pager.AddParamString("q", keyword)
+	pager.AddParamString("type", viewType)
+	pager.AddParamString("sort", sortType)
+	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
+	pager.AddParamString("labels", selectedLabels)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplIssues)
@@ -714,13 +708,17 @@ func UsernameSubRoute(ctx *context.Context) {
 	username := ctx.Params("username")
 	reloadParam := func(suffix string) (success bool) {
 		ctx.SetParams("username", strings.TrimSuffix(username, suffix))
-		context_service.UserAssignmentWeb()(ctx)
+		context.UserAssignmentWeb()(ctx)
+		if ctx.Written() {
+			return false
+		}
+
 		// check view permissions
 		if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
 			ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name))
 			return false
 		}
-		return !ctx.Written()
+		return true
 	}
 	switch {
 	case strings.HasSuffix(username, ".png"):
@@ -741,7 +739,6 @@ func UsernameSubRoute(ctx *context.Context) {
 			return
 		}
 		if reloadParam(".rss") {
-			context_service.UserAssignmentWeb()(ctx)
 			feed.ShowUserFeedRSS(ctx)
 		}
 	case strings.HasSuffix(username, ".atom"):
@@ -753,7 +750,7 @@ func UsernameSubRoute(ctx *context.Context) {
 			feed.ShowUserFeedAtom(ctx)
 		}
 	default:
-		context_service.UserAssignmentWeb()(ctx)
+		context.UserAssignmentWeb()(ctx)
 		if !ctx.Written() {
 			ctx.Data["EnableFeed"] = setting.Other.EnableFeed
 			OwnerProfile(ctx)
@@ -790,22 +787,22 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
 		case issues_model.FilterModeYourRepositories:
 			openClosedOpts.AllPublic = false
 		case issues_model.FilterModeAssign:
-			openClosedOpts.AssigneeID = &doerID
+			openClosedOpts.AssigneeID = optional.Some(doerID)
 		case issues_model.FilterModeCreate:
-			openClosedOpts.PosterID = &doerID
+			openClosedOpts.PosterID = optional.Some(doerID)
 		case issues_model.FilterModeMention:
-			openClosedOpts.MentionID = &doerID
+			openClosedOpts.MentionID = optional.Some(doerID)
 		case issues_model.FilterModeReviewRequested:
-			openClosedOpts.ReviewRequestedID = &doerID
+			openClosedOpts.ReviewRequestedID = optional.Some(doerID)
 		case issues_model.FilterModeReviewed:
-			openClosedOpts.ReviewedID = &doerID
+			openClosedOpts.ReviewedID = optional.Some(doerID)
 		}
-		openClosedOpts.IsClosed = util.OptionalBoolFalse
+		openClosedOpts.IsClosed = optional.Some(false)
 		ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
 		if err != nil {
 			return nil, err
 		}
-		openClosedOpts.IsClosed = util.OptionalBoolTrue
+		openClosedOpts.IsClosed = optional.Some(true)
 		ret.ClosedCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
 		if err != nil {
 			return nil, err
@@ -816,23 +813,23 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
 	if err != nil {
 		return nil, err
 	}
-	ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = &doerID }))
+	ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = optional.Some(doerID) }))
 	if err != nil {
 		return nil, err
 	}
-	ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = &doerID }))
+	ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = optional.Some(doerID) }))
 	if err != nil {
 		return nil, err
 	}
-	ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = &doerID }))
+	ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = optional.Some(doerID) }))
 	if err != nil {
 		return nil, err
 	}
-	ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = &doerID }))
+	ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = optional.Some(doerID) }))
 	if err != nil {
 		return nil, err
 	}
-	ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = &doerID }))
+	ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = optional.Some(doerID) }))
 	if err != nil {
 		return nil, err
 	}
diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go
index a32b015cd1..1cc9886308 100644
--- a/routers/web/user/home_test.go
+++ b/routers/web/user/home_test.go
@@ -10,8 +10,10 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/templates"
+	"code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -113,3 +115,18 @@ func TestMilestonesForSpecificRepo(t *testing.T) {
 	assert.Len(t, ctx.Data["Milestones"], 1)
 	assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2
 }
+
+func TestDashboardPagination(t *testing.T) {
+	ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+	page := context.NewPagination(10, 3, 1, 3)
+
+	setting.AppSubURL = "/SubPath"
+	out, err := ctx.RenderToHTML("base/paginate", map[string]any{"Link": setting.AppSubURL, "Page": page})
+	assert.NoError(t, err)
+	assert.Contains(t, out, `<a class=" item navigation" href="/SubPath/?page=2">`)
+
+	setting.AppSubURL = ""
+	out, err = ctx.RenderToHTML("base/paginate", map[string]any{"Link": setting.AppSubURL, "Page": page})
+	assert.NoError(t, err)
+	assert.Contains(t, out, `<a class=" item navigation" href="/?page=2">`)
+}
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 26f77cfc3a..ae0132e6e2 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -16,11 +16,12 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	issue_service "code.gitea.io/gitea/services/issue"
 	pull_service "code.gitea.io/gitea/services/pull"
 )
@@ -143,6 +144,12 @@ func getNotifications(ctx *context.Context) {
 		ctx.ServerError("LoadIssues", err)
 		return
 	}
+
+	if err = notifications.LoadIssuePullRequests(ctx); err != nil {
+		ctx.ServerError("LoadIssuePullRequests", err)
+		return
+	}
+
 	notifications = notifications.Without(failures)
 	failCount += len(failures)
 
@@ -232,26 +239,25 @@ func NotificationSubscriptions(ctx *context.Context) {
 	if !util.SliceContainsString([]string{"all", "open", "closed"}, state, true) {
 		state = "all"
 	}
+
 	ctx.Data["State"] = state
-	var showClosed util.OptionalBool
+	// default state filter is "all"
+	showClosed := optional.None[bool]()
 	switch state {
-	case "all":
-		showClosed = util.OptionalBoolNone
 	case "closed":
-		showClosed = util.OptionalBoolTrue
+		showClosed = optional.Some(true)
 	case "open":
-		showClosed = util.OptionalBoolFalse
+		showClosed = optional.Some(false)
 	}
 
-	var issueTypeBool util.OptionalBool
 	issueType := ctx.FormString("issueType")
+	// default issue type is no filter
+	issueTypeBool := optional.None[bool]()
 	switch issueType {
 	case "issues":
-		issueTypeBool = util.OptionalBoolFalse
+		issueTypeBool = optional.Some(false)
 	case "pulls":
-		issueTypeBool = util.OptionalBoolTrue
-	default:
-		issueTypeBool = util.OptionalBoolNone
+		issueTypeBool = optional.Some(true)
 	}
 	ctx.Data["IssueType"] = issueType
 
@@ -262,8 +268,7 @@ func NotificationSubscriptions(ctx *context.Context) {
 		var err error
 		labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
 		if err != nil {
-			ctx.ServerError("StringsToInt64s", err)
-			return
+			ctx.Flash.Error(ctx.Tr("invalid_data", selectedLabels), true)
 		}
 	}
 
@@ -344,8 +349,8 @@ func NotificationSubscriptions(ctx *context.Context) {
 		ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current()))
 		return
 	}
-	pager.AddParam(ctx, "sort", "SortType")
-	pager.AddParam(ctx, "state", "State")
+	pager.AddParamString("sort", sortType)
+	pager.AddParamString("state", state)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplNotificationSubscriptions)
@@ -389,6 +394,21 @@ func NotificationWatching(ctx *context.Context) {
 		orderBy = db.SearchOrderByRecentUpdated
 	}
 
+	archived := ctx.FormOptionalBool("archived")
+	ctx.Data["IsArchived"] = archived
+
+	fork := ctx.FormOptionalBool("fork")
+	ctx.Data["IsFork"] = fork
+
+	mirror := ctx.FormOptionalBool("mirror")
+	ctx.Data["IsMirror"] = mirror
+
+	template := ctx.FormOptionalBool("template")
+	ctx.Data["IsTemplate"] = template
+
+	private := ctx.FormOptionalBool("private")
+	ctx.Data["IsPrivate"] = private
+
 	repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
 		ListOptions: db.ListOptions{
 			PageSize: setting.UI.User.RepoPagingNum,
@@ -399,9 +419,14 @@ func NotificationWatching(ctx *context.Context) {
 		OrderBy:            orderBy,
 		Private:            ctx.IsSigned,
 		WatchedByID:        ctx.Doer.ID,
-		Collaborate:        util.OptionalBoolFalse,
+		Collaborate:        optional.Some(false),
 		TopicOnly:          ctx.FormBool("topic"),
 		IncludeDescription: setting.UI.SearchRepoDescription,
+		Archived:           archived,
+		Fork:               fork,
+		Mirror:             mirror,
+		Template:           template,
+		IsPrivate:          private,
 	})
 	if err != nil {
 		ctx.ServerError("SearchRepository", err)
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 708af3e43c..9af49406c4 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -15,8 +15,8 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
 	debian_module "code.gitea.io/gitea/modules/packages/debian"
 	rpm_module "code.gitea.io/gitea/modules/packages/rpm"
@@ -25,6 +25,7 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	packages_helper "code.gitea.io/gitea/routers/api/packages/helper"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	packages_service "code.gitea.io/gitea/services/packages"
 )
@@ -54,7 +55,7 @@ func ListPackages(ctx *context.Context) {
 		OwnerID:    ctx.ContextUser.ID,
 		Type:       packages_model.Type(packageType),
 		Name:       packages_model.SearchValue{Value: query},
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		ctx.ServerError("SearchLatestVersions", err)
@@ -124,8 +125,8 @@ func ListPackages(ctx *context.Context) {
 	}
 
 	pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
-	pager.AddParam(ctx, "q", "Query")
-	pager.AddParam(ctx, "type", "PackageType")
+	pager.AddParamString("q", query)
+	pager.AddParamString("type", packageType)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplPackagesList)
@@ -145,7 +146,7 @@ func RedirectToLastVersion(ctx *context.Context) {
 
 	pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
 		PackageID:  p.ID,
-		IsInternal: util.OptionalBoolFalse,
+		IsInternal: optional.Some(false),
 	})
 	if err != nil {
 		ctx.ServerError("GetPackageByName", err)
@@ -162,7 +163,7 @@ func RedirectToLastVersion(ctx *context.Context) {
 		return
 	}
 
-	ctx.Redirect(pd.FullWebLink())
+	ctx.Redirect(pd.VersionWebLink())
 }
 
 // ViewPackageVersion displays a single package version
@@ -255,7 +256,7 @@ func ViewPackageVersion(ctx *context.Context) {
 		pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 			Paginator:  db.NewAbsoluteListOptions(0, 5),
 			PackageID:  pd.Package.ID,
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 		})
 	}
 	if err != nil {
@@ -359,7 +360,7 @@ func ListPackageVersions(ctx *context.Context) {
 				ExactMatch: false,
 				Value:      query,
 			},
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 			Sort:       sort,
 		})
 		if err != nil {
@@ -467,7 +468,7 @@ func PackageSettingsPost(ctx *context.Context) {
 
 		redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages"
 		// redirect to the package if there are still versions available
-		if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: util.OptionalBoolFalse}); has {
+		if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: optional.Some(false)}); has {
 			redirectURL = ctx.Package.Descriptor.PackageWebLink()
 		}
 
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 73ab93caed..f0749e1021 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -15,20 +15,22 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/feed"
 	"code.gitea.io/gitea/routers/web/org"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
 	tplProfileBigAvatar base.TplName = "shared/user/profile_big_avatar"
+	tplFollowUnfollow   base.TplName = "org/follow_unfollow"
 )
 
 // OwnerProfile render profile page for a user or a organization (aka, repo owner)
@@ -160,6 +162,21 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 	}
 	ctx.Data["NumFollowing"] = numFollowing
 
+	archived := ctx.FormOptionalBool("archived")
+	ctx.Data["IsArchived"] = archived
+
+	fork := ctx.FormOptionalBool("fork")
+	ctx.Data["IsFork"] = fork
+
+	mirror := ctx.FormOptionalBool("mirror")
+	ctx.Data["IsMirror"] = mirror
+
+	template := ctx.FormOptionalBool("template")
+	ctx.Data["IsTemplate"] = template
+
+	private := ctx.FormOptionalBool("private")
+	ctx.Data["IsPrivate"] = private
+
 	switch tab {
 	case "followers":
 		ctx.Data["Cards"] = followers
@@ -202,10 +219,15 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 			OrderBy:            orderBy,
 			Private:            ctx.IsSigned,
 			StarredByID:        ctx.ContextUser.ID,
-			Collaborate:        util.OptionalBoolFalse,
+			Collaborate:        optional.Some(false),
 			TopicOnly:          topicOnly,
 			Language:           language,
 			IncludeDescription: setting.UI.SearchRepoDescription,
+			Archived:           archived,
+			Fork:               fork,
+			Mirror:             mirror,
+			Template:           template,
+			IsPrivate:          private,
 		})
 		if err != nil {
 			ctx.ServerError("SearchRepository", err)
@@ -224,10 +246,15 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 			OrderBy:            orderBy,
 			Private:            ctx.IsSigned,
 			WatchedByID:        ctx.ContextUser.ID,
-			Collaborate:        util.OptionalBoolFalse,
+			Collaborate:        optional.Some(false),
 			TopicOnly:          topicOnly,
 			Language:           language,
 			IncludeDescription: setting.UI.SearchRepoDescription,
+			Archived:           archived,
+			Fork:               fork,
+			Mirror:             mirror,
+			Template:           template,
+			IsPrivate:          private,
 		})
 		if err != nil {
 			ctx.ServerError("SearchRepository", err)
@@ -269,10 +296,15 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 			OwnerID:            ctx.ContextUser.ID,
 			OrderBy:            orderBy,
 			Private:            ctx.IsSigned,
-			Collaborate:        util.OptionalBoolFalse,
+			Collaborate:        optional.Some(false),
 			TopicOnly:          topicOnly,
 			Language:           language,
 			IncludeDescription: setting.UI.SearchRepoDescription,
+			Archived:           archived,
+			Fork:               fork,
+			Mirror:             mirror,
+			Template:           template,
+			IsPrivate:          private,
 		})
 		if err != nil {
 			ctx.ServerError("SearchRepository", err)
@@ -292,12 +324,14 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 
 	pager := context.NewPagination(total, pagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "tab", "TabName")
+	pager.AddParamString("tab", tab)
 	if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" {
-		pager.AddParam(ctx, "language", "Language")
+		pager.AddParamString("language", language)
 	}
 	if tab == "activity" {
-		pager.AddParam(ctx, "date", "Date")
+		if ctx.Data["Date"] != nil {
+			pager.AddParamString("date", fmt.Sprint(ctx.Data["Date"]))
+		}
 	}
 	ctx.Data["Page"] = pager
 }
@@ -307,7 +341,7 @@ func Action(ctx *context.Context) {
 	var err error
 	switch ctx.FormString("action") {
 	case "follow":
-		err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
+		err = user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser)
 	case "unfollow":
 		err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
 	}
@@ -318,6 +352,16 @@ func Action(ctx *context.Context) {
 		return
 	}
 
-	shared_user.PrepareContextForProfileBigAvatar(ctx)
-	ctx.HTML(http.StatusOK, tplProfileBigAvatar)
+	if ctx.ContextUser.IsIndividual() {
+		shared_user.PrepareContextForProfileBigAvatar(ctx)
+		ctx.HTML(http.StatusOK, tplProfileBigAvatar)
+		return
+	} else if ctx.ContextUser.IsOrganization() {
+		ctx.Data["Org"] = ctx.ContextUser
+		ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
+		ctx.HTML(http.StatusOK, tplFollowUnfollow)
+		return
+	}
+	log.Error("Failed to apply action %q: unsupport context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type)
+	ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
 }
diff --git a/routers/web/user/search.go b/routers/web/user/search.go
index 4d090a3784..fb7729bbe1 100644
--- a/routers/web/user/search.go
+++ b/routers/web/user/search.go
@@ -8,7 +8,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index c7f194a3b5..8ea7548e51 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -13,13 +13,15 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/auth/source/db"
+	"code.gitea.io/gitea/services/auth/source/smtp"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
 	"code.gitea.io/gitea/services/user"
@@ -72,7 +74,6 @@ func AccountPost(ctx *context.Context) {
 			case errors.Is(err, password.ErrIsPwned):
 				ctx.Flash.Error(ctx.Tr("auth.password_pwned"))
 			case password.IsErrIsPwnedRequest(err):
-				log.Error("%s", err.Error())
 				ctx.Flash.Error(ctx.Tr("auth.password_pwned_err"))
 			default:
 				ctx.ServerError("UpdateAuth", err)
@@ -92,9 +93,9 @@ func EmailPost(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("settings")
 	ctx.Data["PageIsSettingsAccount"] = true
 
-	// Make emailaddress primary.
+	// Make email address primary.
 	if ctx.FormString("_method") == "PRIMARY" {
-		if err := user_model.MakeEmailPrimary(ctx, &user_model.EmailAddress{ID: ctx.FormInt64("id")}); err != nil {
+		if err := user_model.MakeActiveEmailPrimary(ctx, ctx.FormInt64("id")); err != nil {
 			ctx.ServerError("MakeEmailPrimary", err)
 			return
 		}
@@ -233,15 +234,33 @@ func DeleteEmail(ctx *context.Context) {
 
 // DeleteAccount render user suicide page and response for delete user himself
 func DeleteAccount(ctx *context.Context) {
+	if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureDeletion) {
+		ctx.Error(http.StatusNotFound)
+		return
+	}
+
 	ctx.Data["Title"] = ctx.Tr("settings")
 	ctx.Data["PageIsSettingsAccount"] = true
 
 	if _, _, err := auth.UserSignIn(ctx, ctx.Doer.Name, ctx.FormString("password")); err != nil {
-		if user_model.IsErrUserNotExist(err) {
+		switch {
+		case user_model.IsErrUserNotExist(err):
+			loadAccountData(ctx)
+
+			ctx.RenderWithErr(ctx.Tr("form.user_not_exist"), tplSettingsAccount, nil)
+		case errors.Is(err, smtp.ErrUnsupportedLoginType):
+			loadAccountData(ctx)
+
+			ctx.RenderWithErr(ctx.Tr("form.unsupported_login_type"), tplSettingsAccount, nil)
+		case errors.As(err, &db.ErrUserPasswordNotSet{}):
+			loadAccountData(ctx)
+
+			ctx.RenderWithErr(ctx.Tr("form.unset_password"), tplSettingsAccount, nil)
+		case errors.As(err, &db.ErrUserPasswordInvalid{}):
 			loadAccountData(ctx)
 
 			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil)
-		} else {
+		default:
 			ctx.ServerError("UserSignIn", err)
 		}
 		return
@@ -299,6 +318,7 @@ func loadAccountData(ctx *context.Context) {
 	ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference
 	ctx.Data["ActivationsPending"] = pendingActivation
 	ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
+	ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
 
 	if setting.Service.UserDeleteWithCommentsMaxTime != 0 {
 		ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String()
diff --git a/routers/web/user/setting/account_test.go b/routers/web/user/setting/account_test.go
index 6742c382e9..9fdc5e4d53 100644
--- a/routers/web/user/setting/account_test.go
+++ b/routers/web/user/setting/account_test.go
@@ -8,9 +8,9 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/stretchr/testify/assert"
diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go
index decb35c1e1..171c1933d4 100644
--- a/routers/web/user/setting/adopt.go
+++ b/routers/web/user/setting/adopt.go
@@ -8,9 +8,9 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go
index a7e31fd505..e3822ca988 100644
--- a/routers/web/user/setting/applications.go
+++ b/routers/web/user/setting/applications.go
@@ -10,9 +10,9 @@ import (
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/user/setting/block.go b/routers/web/user/setting/block.go
new file mode 100644
index 0000000000..94fc380cee
--- /dev/null
+++ b/routers/web/user/setting/block.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/setting"
+	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
+)
+
+const (
+	tplSettingsBlockedUsers base.TplName = "user/settings/blocked_users"
+)
+
+func BlockedUsers(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("user.block.list")
+	ctx.Data["PageIsSettingsBlockedUsers"] = true
+
+	shared_user.BlockedUsers(ctx, ctx.Doer)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
+}
+
+func BlockedUsersPost(ctx *context.Context) {
+	shared_user.BlockedUsersPost(ctx, ctx.Doer)
+	if ctx.Written() {
+		return
+	}
+
+	ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users")
+}
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index 16410d06ff..9e969e045d 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -5,15 +5,17 @@
 package setting
 
 import (
+	"fmt"
 	"net/http"
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
@@ -61,7 +63,7 @@ func KeysPost(ctx *context.Context) {
 			ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 			return
 		}
-		if _, err = asymkey_model.AddPrincipalKey(ctx, ctx.Doer.ID, content, 0); err != nil {
+		if _, err = asymkey_service.AddPrincipalKey(ctx, ctx.Doer.ID, content, 0); err != nil {
 			ctx.Data["HasPrincipalError"] = true
 			switch {
 			case asymkey_model.IsErrKeyAlreadyExist(err), asymkey_model.IsErrKeyNameAlreadyUsed(err):
@@ -77,6 +79,11 @@ func KeysPost(ctx *context.Context) {
 		ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 	case "gpg":
+		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
+			ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+			return
+		}
+
 		token := asymkey_model.VerificationToken(ctx.Doer, 1)
 		lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
 
@@ -153,6 +160,11 @@ func KeysPost(ctx *context.Context) {
 		ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID))
 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 	case "ssh":
+		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
+			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+			return
+		}
+
 		content, err := asymkey_model.CheckPublicKeyString(form.Content)
 		if err != nil {
 			if db.IsErrSSHDisabled(err) {
@@ -192,6 +204,11 @@ func KeysPost(ctx *context.Context) {
 		ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 	case "verify_ssh":
+		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
+			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+			return
+		}
+
 		token := asymkey_model.VerificationToken(ctx.Doer, 1)
 		lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
 
@@ -224,12 +241,21 @@ func KeysPost(ctx *context.Context) {
 func DeleteKey(ctx *context.Context) {
 	switch ctx.FormString("type") {
 	case "gpg":
+		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
+			ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+			return
+		}
 		if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil {
 			ctx.Flash.Error("DeleteGPGKey: " + err.Error())
 		} else {
 			ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
 		}
 	case "ssh":
+		if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
+			ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+			return
+		}
+
 		keyID := ctx.FormInt64("id")
 		external, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, keyID)
 		if err != nil {
@@ -308,4 +334,5 @@ func loadKeysData(ctx *context.Context) {
 
 	ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg")
 	ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh")
+	ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
 }
diff --git a/routers/web/user/setting/oauth2.go b/routers/web/user/setting/oauth2.go
index 93142c21fc..1f485e06c8 100644
--- a/routers/web/user/setting/oauth2.go
+++ b/routers/web/user/setting/oauth2.go
@@ -5,8 +5,8 @@ package setting
 
 import (
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/user/setting/oauth2_common.go b/routers/web/user/setting/oauth2_common.go
index fecaa4b873..85d1e820a5 100644
--- a/routers/web/user/setting/oauth2_common.go
+++ b/routers/web/user/setting/oauth2_common.go
@@ -9,10 +9,10 @@ import (
 
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/user/setting/packages.go b/routers/web/user/setting/packages.go
index 34d18f999e..4132659495 100644
--- a/routers/web/user/setting/packages.go
+++ b/routers/web/user/setting/packages.go
@@ -9,11 +9,11 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	chef_module "code.gitea.io/gitea/modules/packages/chef"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	shared "code.gitea.io/gitea/routers/web/shared/packages"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index 95b350528c..49eb050dcb 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -20,7 +20,6 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
@@ -29,6 +28,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	user_service "code.gitea.io/gitea/services/user"
 )
@@ -126,7 +126,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
 		defer fr.Close()
 
 		if form.Avatar.Size > setting.Avatar.MaxFileSize {
-			return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
+			return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
 		}
 
 		data, err := io.ReadAll(fr)
@@ -136,7 +136,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
 
 		st := typesniffer.DetectContentType(data)
 		if !(st.IsImage() && !st.IsSvgImage()) {
-			return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
+			return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image"))
 		}
 		if err = user_service.UploadAvatar(ctx, ctxUser, data); err != nil {
 			return fmt.Errorf("UploadAvatar: %w", err)
@@ -389,7 +389,7 @@ func UpdateUserLang(ctx *context.Context) {
 	middleware.SetLocaleCookie(ctx.Resp, ctx.Doer.Language, 0)
 
 	log.Trace("User settings updated: %s", ctx.Doer.Name)
-	ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).Tr("settings.update_language_success"))
+	ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).TrString("settings.update_language_success"))
 	ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
 }
 
diff --git a/routers/web/user/setting/runner.go b/routers/web/user/setting/runner.go
index 451fd0ca97..2bb10cceb9 100644
--- a/routers/web/user/setting/runner.go
+++ b/routers/web/user/setting/runner.go
@@ -4,8 +4,8 @@
 package setting
 
 import (
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go
index 7858b634ce..cd09102369 100644
--- a/routers/web/user/setting/security/2fa.go
+++ b/routers/web/user/setting/security/2fa.go
@@ -13,10 +13,10 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models/auth"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/pquerna/otp"
diff --git a/routers/web/user/setting/security/openid.go b/routers/web/user/setting/security/openid.go
index 9a207e149d..8f788e1735 100644
--- a/routers/web/user/setting/security/openid.go
+++ b/routers/web/user/setting/security/openid.go
@@ -8,10 +8,10 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/openid"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go
index 3647d606ee..8d6859ab87 100644
--- a/routers/web/user/setting/security/security.go
+++ b/routers/web/user/setting/security/security.go
@@ -12,10 +12,10 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
@@ -112,7 +112,7 @@ func loadSecurityData(ctx *context.Context) {
 	ctx.Data["AccountLinks"] = sources
 
 	authSources, err := db.Find[auth_model.Source](ctx, auth_model.FindSourcesOptions{
-		IsActive:  util.OptionalBoolNone,
+		IsActive:  optional.None[bool](),
 		LoginType: auth_model.OAuth2,
 	})
 	if err != nil {
diff --git a/routers/web/user/setting/security/webauthn.go b/routers/web/user/setting/security/webauthn.go
index ce103528c5..e382c8b9af 100644
--- a/routers/web/user/setting/security/webauthn.go
+++ b/routers/web/user/setting/security/webauthn.go
@@ -11,10 +11,10 @@ import (
 
 	"code.gitea.io/gitea/models/auth"
 	wa "code.gitea.io/gitea/modules/auth/webauthn"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 
 	"github.com/go-webauthn/webauthn/protocol"
diff --git a/routers/web/user/setting/webhooks.go b/routers/web/user/setting/webhooks.go
index 679b72e501..4423b62781 100644
--- a/routers/web/user/setting/webhooks.go
+++ b/routers/web/user/setting/webhooks.go
@@ -9,8 +9,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 const (
diff --git a/routers/web/user/stop_watch.go b/routers/web/user/stop_watch.go
index 86f66e64a6..38f74ea455 100644
--- a/routers/web/user/stop_watch.go
+++ b/routers/web/user/stop_watch.go
@@ -8,7 +8,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/routers/web/user/task.go b/routers/web/user/task.go
index f35f40e6a0..8476767e9e 100644
--- a/routers/web/user/task.go
+++ b/routers/web/user/task.go
@@ -8,8 +8,8 @@ import (
 	"strconv"
 
 	admin_model "code.gitea.io/gitea/models/admin"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/services/context"
 )
 
 // TaskStatus returns task's status
@@ -39,7 +39,7 @@ func TaskStatus(ctx *context.Context) {
 				Args:   []any{task.Message},
 			}
 		}
-		message = ctx.Tr(translatableMessage.Format, translatableMessage.Args...)
+		message = ctx.Locale.TrString(translatableMessage.Format, translatableMessage.Args...)
 	}
 
 	ctx.JSON(http.StatusOK, map[string]any{
diff --git a/routers/web/web.go b/routers/web/web.go
index 92cf5132b4..4fff994e42 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -12,7 +12,6 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	"code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/metrics"
 	"code.gitea.io/gitea/modules/public"
@@ -42,7 +41,7 @@ import (
 	user_setting "code.gitea.io/gitea/routers/web/user/setting"
 	"code.gitea.io/gitea/routers/web/user/setting/security"
 	auth_service "code.gitea.io/gitea/services/auth"
-	context_service "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/lfs"
 
@@ -155,7 +154,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
 			if ctx.Doer.MustChangePassword {
 				if ctx.Req.URL.Path != "/user/settings/change_password" {
 					if strings.HasPrefix(ctx.Req.UserAgent(), "git") {
-						ctx.Error(http.StatusUnauthorized, ctx.Tr("auth.must_change_password"))
+						ctx.Error(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password"))
 						return
 					}
 					ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
@@ -175,7 +174,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
 
 		// Redirect to dashboard (or alternate location) if user tries to visit any non-login page.
 		if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" {
-			ctx.RedirectToFirst(ctx.FormString("redirect_to"))
+			ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"))
 			return
 		}
 
@@ -473,6 +472,7 @@ func registerRoutes(m *web.Route) {
 		m.Get("/change-password", func(ctx *context.Context) {
 			ctx.Redirect(setting.AppSubURL + "/user/settings/account")
 		})
+		m.Get("/passkey-endpoints", passkeyEndpoints)
 		m.Methods("GET, HEAD", "/*", public.FileHandlerFunc())
 	}, optionsCorsHandler())
 
@@ -647,6 +647,11 @@ func registerRoutes(m *web.Route) {
 			})
 			addWebhookEditRoutes()
 		}, webhooksEnabled)
+
+		m.Group("/blocked_users", func() {
+			m.Get("", user_setting.BlockedUsers)
+			m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost)
+		})
 	}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
 
 	m.Group("/user", func() {
@@ -676,6 +681,7 @@ func registerRoutes(m *web.Route) {
 	// ***** START: Admin *****
 	m.Group("/admin", func() {
 		m.Get("", admin.Dashboard)
+		m.Get("/system_status", admin.SystemStatus)
 		m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost)
 
 		m.Get("/self_check", admin.SelfCheck)
@@ -684,6 +690,7 @@ func registerRoutes(m *web.Route) {
 			m.Get("", admin.Config)
 			m.Post("", admin.ChangeConfig)
 			m.Post("/test_mail", admin.SendTestMail)
+			m.Get("/settings", admin.ConfigSettings)
 		})
 
 		m.Group("/monitor", func() {
@@ -787,7 +794,7 @@ func registerRoutes(m *web.Route) {
 		m.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), repo.GetAttachment)
 	}, ignSignIn)
 
-	m.Post("/{username}", reqSignIn, context_service.UserAssignmentWeb(), user.Action)
+	m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), user.Action)
 
 	reqRepoAdmin := context.RequireRepoAdmin()
 	reqRepoCodeWriter := context.RequireRepoWriter(unit.TypeCode)
@@ -943,6 +950,11 @@ func registerRoutes(m *web.Route) {
 						m.Post("/rebuild", org.RebuildCargoIndex)
 					})
 				}, packagesEnabled)
+
+				m.Group("/blocked_users", func() {
+					m.Get("", org.BlockedUsers)
+					m.Post("", web.Bind(forms.BlockUserForm{}), org.BlockedUsersPost)
+				})
 			}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
 		}, context.OrgAssignment(true, true))
 	}, reqSignIn)
@@ -954,10 +966,6 @@ func registerRoutes(m *web.Route) {
 		m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost)
 		m.Get("/migrate", repo.Migrate)
 		m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost)
-		m.Group("/fork", func() {
-			m.Combo("/{repoid}").Get(repo.Fork).
-				Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
-		}, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader)
 		m.Get("/search", repo.SearchRepo)
 	}, reqSignIn)
 
@@ -1000,7 +1008,6 @@ func registerRoutes(m *web.Route) {
 						m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard)
 						m.Delete("", org.DeleteProjectBoard)
 						m.Post("/default", org.SetDefaultProjectBoard)
-						m.Post("/unsetdefault", org.UnsetDefaultProjectBoard)
 
 						m.Post("/move", org.MoveIssues)
 					})
@@ -1016,7 +1023,7 @@ func registerRoutes(m *web.Route) {
 		m.Group("", func() {
 			m.Get("/code", user.CodeSearch)
 		}, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false), individualPermsChecker)
-	}, ignSignIn, context_service.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code)
+	}, ignSignIn, context.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code)
 
 	m.Group("/{username}/{reponame}", func() {
 		m.Group("/settings", func() {
@@ -1253,6 +1260,8 @@ func registerRoutes(m *web.Route) {
 			m.Post("/delete", repo.DeleteBranchPost)
 			m.Post("/restore", repo.RestoreBranchPost)
 		}, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
+
+		m.Combo("/fork", reqRepoCodeReader).Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
 	}, reqSignIn, context.RepoAssignment, context.UnitTypes())
 
 	// Tags
@@ -1338,13 +1347,12 @@ func registerRoutes(m *web.Route) {
 						m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
 						m.Delete("", repo.DeleteProjectBoard)
 						m.Post("/default", repo.SetDefaultProjectBoard)
-						m.Post("/unsetdefault", repo.UnSetDefaultProjectBoard)
 
 						m.Post("/move", repo.MoveIssues)
 					})
 				})
 			}, reqRepoProjectsWriter, context.RepoMustNotBeArchived())
-		}, reqRepoProjectsReader, repo.MustEnableProjects)
+		}, reqRepoProjectsReader, repo.MustEnableRepoProjects)
 
 		m.Group("/actions", func() {
 			m.Get("", actions.List)
@@ -1364,10 +1372,14 @@ func registerRoutes(m *web.Route) {
 				})
 				m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
 				m.Post("/approve", reqRepoActionsWriter, actions.Approve)
-				m.Post("/artifacts", actions.ArtifactsView)
+				m.Get("/artifacts", actions.ArtifactsView)
 				m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
+				m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView)
 				m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
 			})
+			m.Group("/workflows/{workflow_name}", func() {
+				m.Get("/badge.svg", actions.GetWorkflowBadge)
+			})
 		}, reqRepoActionsReader, actions.MustEnableActions)
 
 		m.Group("/wiki", func() {
@@ -1391,6 +1403,18 @@ func registerRoutes(m *web.Route) {
 		m.Group("/activity", func() {
 			m.Get("", repo.Activity)
 			m.Get("/{period}", repo.Activity)
+			m.Group("/contributors", func() {
+				m.Get("", repo.Contributors)
+				m.Get("/data", repo.ContributorsData)
+			})
+			m.Group("/code-frequency", func() {
+				m.Get("", repo.CodeFrequency)
+				m.Get("/data", repo.CodeFrequencyData)
+			})
+			m.Group("/recent-commits", func() {
+				m.Get("", repo.RecentCommits)
+				m.Get("/data", repo.RecentCommitsData)
+			})
 		}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
 
 		m.Group("/activity_author_data", func() {
diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go
index faa35b8d2f..a87c426b3b 100644
--- a/routers/web/webfinger.go
+++ b/routers/web/webfinger.go
@@ -10,9 +10,9 @@ import (
 	"strings"
 
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 // https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-webfinger-14#section-4.4
diff --git a/services/actions/auth.go b/services/actions/auth.go
index 53e68f0b71..8e934d89a8 100644
--- a/services/actions/auth.go
+++ b/services/actions/auth.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"time"
 
+	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 
@@ -21,24 +22,48 @@ type actionsClaims struct {
 	TaskID int64
 	RunID  int64
 	JobID  int64
+	Ac     string `json:"ac"`
 }
 
+type actionsCacheScope struct {
+	Scope      string
+	Permission actionsCachePermission
+}
+
+type actionsCachePermission int
+
+const (
+	actionsCachePermissionRead = 1 << iota
+	actionsCachePermissionWrite
+)
+
 func CreateAuthorizationToken(taskID, runID, jobID int64) (string, error) {
 	now := time.Now()
 
+	ac, err := json.Marshal(&[]actionsCacheScope{
+		{
+			Scope:      "",
+			Permission: actionsCachePermissionWrite,
+		},
+	})
+	if err != nil {
+		return "", err
+	}
+
 	claims := actionsClaims{
 		RegisteredClaims: jwt.RegisteredClaims{
 			ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
 			NotBefore: jwt.NewNumericDate(now),
 		},
 		Scp:    fmt.Sprintf("Actions.Results:%d:%d", runID, jobID),
+		Ac:     string(ac),
 		TaskID: taskID,
 		RunID:  runID,
 		JobID:  jobID,
 	}
 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
 
-	tokenString, err := token.SignedString([]byte(setting.SecretKey))
+	tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
 	if err != nil {
 		return "", err
 	}
@@ -62,7 +87,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
 		if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
 			return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
 		}
-		return []byte(setting.SecretKey), nil
+		return setting.GetGeneralTokenSigningSecret(), nil
 	})
 	if err != nil {
 		return 0, err
diff --git a/services/actions/auth_test.go b/services/actions/auth_test.go
index f6288ccd5a..f73ae8ae4c 100644
--- a/services/actions/auth_test.go
+++ b/services/actions/auth_test.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"testing"
 
+	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/golang-jwt/jwt/v5"
@@ -20,7 +21,7 @@ func TestCreateAuthorizationToken(t *testing.T) {
 	assert.NotEqual(t, "", token)
 	claims := jwt.MapClaims{}
 	_, err = jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
-		return []byte(setting.SecretKey), nil
+		return setting.GetGeneralTokenSigningSecret(), nil
 	})
 	assert.Nil(t, err)
 	scp, ok := claims["scp"]
@@ -29,6 +30,14 @@ func TestCreateAuthorizationToken(t *testing.T) {
 	taskIDClaim, ok := claims["TaskID"]
 	assert.True(t, ok, "Has TaskID claim in jwt token")
 	assert.Equal(t, float64(taskID), taskIDClaim, "Supplied taskid must match stored one")
+	acClaim, ok := claims["ac"]
+	assert.True(t, ok, "Has ac claim in jwt token")
+	ac, ok := acClaim.(string)
+	assert.True(t, ok, "ac claim is a string for buildx gha cache")
+	scopes := []actionsCacheScope{}
+	err = json.Unmarshal([]byte(ac), &scopes)
+	assert.NoError(t, err, "ac claim is a json list for buildx gha cache")
+	assert.GreaterOrEqual(t, len(scopes), 1, "Expected at least one action cache scope for buildx gha cache")
 }
 
 func TestParseAuthorizationToken(t *testing.T) {
diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go
index 785eeb5838..5376c2624c 100644
--- a/services/actions/cleanup.go
+++ b/services/actions/cleanup.go
@@ -20,23 +20,59 @@ func Cleanup(taskCtx context.Context, olderThan time.Duration) error {
 	return CleanupArtifacts(taskCtx)
 }
 
-// CleanupArtifacts removes expired artifacts and set records expired status
+// CleanupArtifacts removes expired add need-deleted artifacts and set records expired status
 func CleanupArtifacts(taskCtx context.Context) error {
+	if err := cleanExpiredArtifacts(taskCtx); err != nil {
+		return err
+	}
+	return cleanNeedDeleteArtifacts(taskCtx)
+}
+
+func cleanExpiredArtifacts(taskCtx context.Context) error {
 	artifacts, err := actions.ListNeedExpiredArtifacts(taskCtx)
 	if err != nil {
 		return err
 	}
 	log.Info("Found %d expired artifacts", len(artifacts))
 	for _, artifact := range artifacts {
-		if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
-			log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
-			continue
-		}
 		if err := actions.SetArtifactExpired(taskCtx, artifact.ID); err != nil {
 			log.Error("Cannot set artifact %d expired: %v", artifact.ID, err)
 			continue
 		}
+		if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
+			log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
+			continue
+		}
 		log.Info("Artifact %d set expired", artifact.ID)
 	}
 	return nil
 }
+
+// deleteArtifactBatchSize is the batch size of deleting artifacts
+const deleteArtifactBatchSize = 100
+
+func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
+	for {
+		artifacts, err := actions.ListPendingDeleteArtifacts(taskCtx, deleteArtifactBatchSize)
+		if err != nil {
+			return err
+		}
+		log.Info("Found %d artifacts pending deletion", len(artifacts))
+		for _, artifact := range artifacts {
+			if err := actions.SetArtifactDeleted(taskCtx, artifact.ID); err != nil {
+				log.Error("Cannot set artifact %d deleted: %v", artifact.ID, err)
+				continue
+			}
+			if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
+				log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
+				continue
+			}
+			log.Info("Artifact %d set deleted", artifact.ID)
+		}
+		if len(artifacts) < deleteArtifactBatchSize {
+			log.Debug("No more artifacts pending deletion")
+			break
+		}
+	}
+	return nil
+}
diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go
index 72a3ab7ac6..eb031511f6 100644
--- a/services/actions/commit_status.go
+++ b/services/actions/commit_status.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
+	commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
 
 	"github.com/nektos/act/pkg/jobparser"
 )
@@ -64,6 +65,9 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
 			return fmt.Errorf("head of pull request is missing in event payload")
 		}
 		sha = payload.PullRequest.Head.Sha
+	case webhook_module.HookEventRelease:
+		event = string(run.Event)
+		sha = run.CommitSHA
 	default:
 		return nil
 	}
@@ -76,7 +80,7 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
 	}
 	ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)
 	state := toCommitStatus(job.Status)
-	if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true}); err == nil {
+	if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll); err == nil {
 		for _, v := range statuses {
 			if v.Context == ctxname {
 				if v.State == state {
@@ -119,18 +123,13 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
 	if err != nil {
 		return fmt.Errorf("HashTypeInterfaceFromHashString: %w", err)
 	}
-	if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
-		Repo:    repo,
-		SHA:     commitID,
-		Creator: creator,
-		CommitStatus: &git_model.CommitStatus{
-			SHA:         sha,
-			TargetURL:   fmt.Sprintf("%s/jobs/%d", run.Link(), index),
-			Description: description,
-			Context:     ctxname,
-			CreatorID:   creator.ID,
-			State:       state,
-		},
+	if err := commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &git_model.CommitStatus{
+		SHA:         sha,
+		TargetURL:   fmt.Sprintf("%s/jobs/%d", run.Link(), index),
+		Description: description,
+		Context:     ctxname,
+		CreatorID:   creator.ID,
+		State:       state,
 	}); err != nil {
 		return fmt.Errorf("NewCommitStatus: %w", err)
 	}
diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go
index fe39312386..d2bbbd9a7c 100644
--- a/services/actions/job_emitter.go
+++ b/services/actions/job_emitter.go
@@ -7,12 +7,14 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"strings"
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/queue"
 
+	"github.com/nektos/act/pkg/jobparser"
 	"xorm.io/builder"
 )
 
@@ -76,12 +78,15 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
 type jobStatusResolver struct {
 	statuses map[int64]actions_model.Status
 	needs    map[int64][]int64
+	jobMap   map[int64]*actions_model.ActionRunJob
 }
 
 func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
 	idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
+	jobMap := make(map[int64]*actions_model.ActionRunJob)
 	for _, job := range jobs {
 		idToJobs[job.JobID] = append(idToJobs[job.JobID], job)
+		jobMap[job.ID] = job
 	}
 
 	statuses := make(map[int64]actions_model.Status, len(jobs))
@@ -97,6 +102,7 @@ func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
 	return &jobStatusResolver{
 		statuses: statuses,
 		needs:    needs,
+		jobMap:   jobMap,
 	}
 }
 
@@ -135,7 +141,20 @@ func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
 			if allSucceed {
 				ret[id] = actions_model.StatusWaiting
 			} else {
-				ret[id] = actions_model.StatusSkipped
+				// If a job's "if" condition is "always()", the job should always run even if some of its dependencies did not succeed.
+				// See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds
+				always := false
+				if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload); len(wfJobs) == 1 {
+					_, wfJob := wfJobs[0].Job()
+					expr := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(wfJob.If.Value, "${{"), "}}"))
+					always = expr == "always()"
+				}
+
+				if always {
+					ret[id] = actions_model.StatusWaiting
+				} else {
+					ret[id] = actions_model.StatusSkipped
+				}
 			}
 		}
 	}
diff --git a/services/actions/job_emitter_test.go b/services/actions/job_emitter_test.go
index e81aa61d80..038df7d4f8 100644
--- a/services/actions/job_emitter_test.go
+++ b/services/actions/job_emitter_test.go
@@ -70,6 +70,62 @@ func Test_jobStatusResolver_Resolve(t *testing.T) {
 			},
 			want: map[int64]actions_model.Status{},
 		},
+		{
+			name: "with ${{ always() }} condition",
+			jobs: actions_model.ActionJobList{
+				{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
+				{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
+					`
+name: test
+on: push
+jobs:
+  job2:
+    runs-on: ubuntu-latest
+    needs: job1
+    if: ${{ always() }}
+    steps:
+      - run: echo "always run"
+`)},
+			},
+			want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
+		},
+		{
+			name: "with always() condition",
+			jobs: actions_model.ActionJobList{
+				{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
+				{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
+					`
+name: test
+on: push
+jobs:
+  job2:
+    runs-on: ubuntu-latest
+    needs: job1
+    if: always()
+    steps:
+      - run: echo "always run"
+`)},
+			},
+			want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
+		},
+		{
+			name: "without always() condition",
+			jobs: actions_model.ActionJobList{
+				{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
+				{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
+					`
+name: test
+on: push
+jobs:
+  job2:
+    runs-on: ubuntu-latest
+    needs: job1
+    steps:
+      - run: echo "not always run"
+`)},
+			},
+			want: map[int64]actions_model.Status{2: actions_model.StatusSkipped},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index 39dfbff283..d7602b79ae 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -49,12 +49,53 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu
 	newNotifyInputFromIssue(issue, webhook_module.HookEventIssues).WithPayload(&api.IssuePayload{
 		Action:     api.HookIssueOpened,
 		Index:      issue.Index,
-		Issue:      convert.ToAPIIssue(ctx, issue),
+		Issue:      convert.ToAPIIssue(ctx, issue.Poster, issue),
 		Repository: convert.ToRepo(ctx, issue.Repo, permission),
 		Sender:     convert.ToUser(ctx, issue.Poster, nil),
 	}).Notify(withMethod(ctx, "NewIssue"))
 }
 
+// IssueChangeContent notifies change content of issue
+func (n *actionsNotifier) IssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) {
+	ctx = withMethod(ctx, "IssueChangeContent")
+
+	var err error
+	if err = issue.LoadRepo(ctx); err != nil {
+		log.Error("LoadRepo: %v", err)
+		return
+	}
+
+	permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
+	if issue.IsPull {
+		if err = issue.LoadPullRequest(ctx); err != nil {
+			log.Error("loadPullRequest: %v", err)
+			return
+		}
+		newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequest).
+			WithDoer(doer).
+			WithPayload(&api.PullRequestPayload{
+				Action:      api.HookIssueEdited,
+				Index:       issue.Index,
+				PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+				Repository:  convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
+				Sender:      convert.ToUser(ctx, doer, nil),
+			}).
+			WithPullRequest(issue.PullRequest).
+			Notify(ctx)
+		return
+	}
+	newNotifyInputFromIssue(issue, webhook_module.HookEventIssues).
+		WithDoer(doer).
+		WithPayload(&api.IssuePayload{
+			Action:     api.HookIssueEdited,
+			Index:      issue.Index,
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
+			Repository: convert.ToRepo(ctx, issue.Repo, permission),
+			Sender:     convert.ToUser(ctx, doer, nil),
+		}).
+		Notify(ctx)
+}
+
 // IssueChangeStatus notifies close or reopen issue to notifiers
 func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, _ *issues_model.Comment, isClosed bool) {
 	ctx = withMethod(ctx, "IssueChangeStatus")
@@ -86,7 +127,7 @@ func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode
 	}
 	apiIssue := &api.IssuePayload{
 		Index:      issue.Index,
-		Issue:      convert.ToAPIIssue(ctx, issue),
+		Issue:      convert.ToAPIIssue(ctx, doer, issue),
 		Repository: convert.ToRepo(ctx, issue.Repo, permission),
 		Sender:     convert.ToUser(ctx, doer, nil),
 	}
@@ -101,11 +142,58 @@ func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode
 		Notify(ctx)
 }
 
+// IssueChangeAssignee notifies assigned or unassigned to notifiers
+func (n *actionsNotifier) IssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
+	ctx = withMethod(ctx, "IssueChangeAssignee")
+
+	var action api.HookIssueAction
+	if removed {
+		action = api.HookIssueUnassigned
+	} else {
+		action = api.HookIssueAssigned
+	}
+
+	hookEvent := webhook_module.HookEventIssueAssign
+	if issue.IsPull {
+		hookEvent = webhook_module.HookEventPullRequestAssign
+	}
+
+	notifyIssueChange(ctx, doer, issue, hookEvent, action)
+}
+
+// IssueChangeMilestone notifies assignee to notifiers
+func (n *actionsNotifier) IssueChangeMilestone(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) {
+	ctx = withMethod(ctx, "IssueChangeMilestone")
+
+	var action api.HookIssueAction
+	if issue.MilestoneID > 0 {
+		action = api.HookIssueMilestoned
+	} else {
+		action = api.HookIssueDemilestoned
+	}
+
+	hookEvent := webhook_module.HookEventIssueMilestone
+	if issue.IsPull {
+		hookEvent = webhook_module.HookEventPullRequestMilestone
+	}
+
+	notifyIssueChange(ctx, doer, issue, hookEvent, action)
+}
+
 func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue,
 	_, _ []*issues_model.Label,
 ) {
 	ctx = withMethod(ctx, "IssueChangeLabels")
 
+	hookEvent := webhook_module.HookEventIssueLabel
+	if issue.IsPull {
+		hookEvent = webhook_module.HookEventPullRequestLabel
+	}
+
+	notifyIssueChange(ctx, doer, issue, hookEvent, api.HookIssueLabelUpdated)
+}
+
+func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction) {
 	var err error
 	if err = issue.LoadRepo(ctx); err != nil {
 		log.Error("LoadRepo: %v", err)
@@ -117,20 +205,15 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
 		return
 	}
 
-	permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
 	if issue.IsPull {
 		if err = issue.LoadPullRequest(ctx); err != nil {
 			log.Error("loadPullRequest: %v", err)
 			return
 		}
-		if err = issue.PullRequest.LoadIssue(ctx); err != nil {
-			log.Error("LoadIssue: %v", err)
-			return
-		}
-		newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestLabel).
+		newNotifyInputFromIssue(issue, event).
 			WithDoer(doer).
 			WithPayload(&api.PullRequestPayload{
-				Action:      api.HookIssueLabelUpdated,
+				Action:      action,
 				Index:       issue.Index,
 				PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
 				Repository:  convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
@@ -140,12 +223,13 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
 			Notify(ctx)
 		return
 	}
-	newNotifyInputFromIssue(issue, webhook_module.HookEventIssueLabel).
+	permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
+	newNotifyInputFromIssue(issue, event).
 		WithDoer(doer).
 		WithPayload(&api.IssuePayload{
-			Action:     api.HookIssueLabelUpdated,
+			Action:     action,
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		}).
@@ -158,37 +242,88 @@ func (n *actionsNotifier) CreateIssueComment(ctx context.Context, doer *user_mod
 ) {
 	ctx = withMethod(ctx, "CreateIssueComment")
 
-	permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer)
-
 	if issue.IsPull {
-		if err := issue.LoadPullRequest(ctx); err != nil {
+		notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventPullRequestComment, api.HookIssueCommentCreated)
+		return
+	}
+	notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventIssueComment, api.HookIssueCommentCreated)
+}
+
+func (n *actionsNotifier) UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
+	ctx = withMethod(ctx, "UpdateComment")
+
+	if err := c.LoadIssue(ctx); err != nil {
+		log.Error("LoadIssue: %v", err)
+		return
+	}
+
+	if c.Issue.IsPull {
+		notifyIssueCommentChange(ctx, doer, c, oldContent, webhook_module.HookEventPullRequestComment, api.HookIssueCommentEdited)
+		return
+	}
+	notifyIssueCommentChange(ctx, doer, c, oldContent, webhook_module.HookEventIssueComment, api.HookIssueCommentEdited)
+}
+
+func (n *actionsNotifier) DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) {
+	ctx = withMethod(ctx, "DeleteComment")
+
+	if err := comment.LoadIssue(ctx); err != nil {
+		log.Error("LoadIssue: %v", err)
+		return
+	}
+
+	if comment.Issue.IsPull {
+		notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventPullRequestComment, api.HookIssueCommentDeleted)
+		return
+	}
+	notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventIssueComment, api.HookIssueCommentDeleted)
+}
+
+func notifyIssueCommentChange(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, oldContent string, event webhook_module.HookEventType, action api.HookIssueCommentAction) {
+	if err := comment.LoadIssue(ctx); err != nil {
+		log.Error("LoadIssue: %v", err)
+		return
+	}
+	if err := comment.Issue.LoadAttributes(ctx); err != nil {
+		log.Error("LoadAttributes: %v", err)
+		return
+	}
+
+	permission, _ := access_model.GetUserRepoPermission(ctx, comment.Issue.Repo, doer)
+
+	payload := &api.IssueCommentPayload{
+		Action:     action,
+		Issue:      convert.ToAPIIssue(ctx, doer, comment.Issue),
+		Comment:    convert.ToAPIComment(ctx, comment.Issue.Repo, comment),
+		Repository: convert.ToRepo(ctx, comment.Issue.Repo, permission),
+		Sender:     convert.ToUser(ctx, doer, nil),
+		IsPull:     comment.Issue.IsPull,
+	}
+
+	if action == api.HookIssueCommentEdited {
+		payload.Changes = &api.ChangesPayload{
+			Body: &api.ChangesFromPayload{
+				From: oldContent,
+			},
+		}
+	}
+
+	if comment.Issue.IsPull {
+		if err := comment.Issue.LoadPullRequest(ctx); err != nil {
 			log.Error("LoadPullRequest: %v", err)
 			return
 		}
-		newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestComment).
+		newNotifyInputFromIssue(comment.Issue, event).
 			WithDoer(doer).
-			WithPayload(&api.IssueCommentPayload{
-				Action:     api.HookIssueCommentCreated,
-				Issue:      convert.ToAPIIssue(ctx, issue),
-				Comment:    convert.ToAPIComment(ctx, repo, comment),
-				Repository: convert.ToRepo(ctx, repo, permission),
-				Sender:     convert.ToUser(ctx, doer, nil),
-				IsPull:     true,
-			}).
-			WithPullRequest(issue.PullRequest).
+			WithPayload(payload).
+			WithPullRequest(comment.Issue.PullRequest).
 			Notify(ctx)
 		return
 	}
-	newNotifyInputFromIssue(issue, webhook_module.HookEventIssueComment).
+
+	newNotifyInputFromIssue(comment.Issue, event).
 		WithDoer(doer).
-		WithPayload(&api.IssueCommentPayload{
-			Action:     api.HookIssueCommentCreated,
-			Issue:      convert.ToAPIIssue(ctx, issue),
-			Comment:    convert.ToAPIComment(ctx, repo, comment),
-			Repository: convert.ToRepo(ctx, repo, permission),
-			Sender:     convert.ToUser(ctx, doer, nil),
-			IsPull:     false,
-		}).
+		WithPayload(payload).
 		Notify(ctx)
 }
 
@@ -305,6 +440,39 @@ func (n *actionsNotifier) PullRequestReview(ctx context.Context, pr *issues_mode
 		}).Notify(ctx)
 }
 
+func (n *actionsNotifier) PullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
+	if !issue.IsPull {
+		log.Warn("PullRequestReviewRequest: issue is not a pull request: %v", issue.ID)
+		return
+	}
+
+	ctx = withMethod(ctx, "PullRequestReviewRequest")
+
+	permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
+	if err := issue.LoadPullRequest(ctx); err != nil {
+		log.Error("LoadPullRequest failed: %v", err)
+		return
+	}
+	var action api.HookIssueAction
+	if isRequest {
+		action = api.HookIssueReviewRequested
+	} else {
+		action = api.HookIssueReviewRequestRemoved
+	}
+	newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestReviewRequest).
+		WithDoer(doer).
+		WithPayload(&api.PullRequestPayload{
+			Action:            action,
+			Index:             issue.Index,
+			PullRequest:       convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+			RequestedReviewer: convert.ToUser(ctx, reviewer, nil),
+			Repository:        convert.ToRepo(ctx, issue.Repo, permission),
+			Sender:            convert.ToUser(ctx, doer, nil),
+		}).
+		WithPullRequest(issue.PullRequest).
+		Notify(ctx)
+}
+
 func (*actionsNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
 	ctx = withMethod(ctx, "MergePullRequest")
 
@@ -347,6 +515,12 @@ func (*actionsNotifier) MergePullRequest(ctx context.Context, doer *user_model.U
 }
 
 func (n *actionsNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
+	commitID, _ := git.NewIDFromString(opts.NewCommitID)
+	if commitID.IsZero() {
+		log.Trace("new commitID is empty")
+		return
+	}
+
 	ctx = withMethod(ctx, "PushCommits")
 
 	apiPusher := convert.ToUser(ctx, pusher, nil)
@@ -379,9 +553,9 @@ func (n *actionsNotifier) CreateRef(ctx context.Context, pusher *user_model.User
 	apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeNone})
 
 	newNotifyInput(repo, pusher, webhook_module.HookEventCreate).
-		WithRef(refFullName.ShortName()). // FIXME: should we use a full ref name
+		WithRef(refFullName.String()).
 		WithPayload(&api.CreatePayload{
-			Ref:     refFullName.ShortName(),
+			Ref:     refFullName.String(),
 			Sha:     refID,
 			RefType: refFullName.RefType(),
 			Repo:    apiRepo,
@@ -397,9 +571,8 @@ func (n *actionsNotifier) DeleteRef(ctx context.Context, pusher *user_model.User
 	apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeNone})
 
 	newNotifyInput(repo, pusher, webhook_module.HookEventDelete).
-		WithRef(refFullName.ShortName()). // FIXME: should we use a full ref name
 		WithPayload(&api.DeletePayload{
-			Ref:        refFullName.ShortName(),
+			Ref:        refFullName.String(),
 			RefType:    refFullName.RefType(),
 			PusherType: api.PusherTypeUser,
 			Repo:       apiRepo,
@@ -456,6 +629,10 @@ func (n *actionsNotifier) UpdateRelease(ctx context.Context, doer *user_model.Us
 }
 
 func (n *actionsNotifier) DeleteRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) {
+	if rel.IsTag {
+		// has sent same action in `PushCommits`, so skip it.
+		return
+	}
 	ctx = withMethod(ctx, "DeleteRelease")
 	notifyRelease(ctx, doer, rel, api.HookReleaseDeleted)
 }
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index 77173e58a3..c48886a824 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -117,6 +117,9 @@ func notify(ctx context.Context, input *notifyInput) error {
 		log.Debug("ignore executing %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
 		return nil
 	}
+	if input.Repo.IsEmpty || input.Repo.IsArchived {
+		return nil
+	}
 	if unit_model.TypeActions.UnitGlobalDisabled() {
 		if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
 			log.Error("CleanRepoScheduleTasks: %v", err)
@@ -136,12 +139,15 @@ func notify(ctx context.Context, input *notifyInput) error {
 	defer gitRepo.Close()
 
 	ref := input.Ref
-	if input.Event == webhook_module.HookEventDelete {
-		// The event is deleting a reference, so it will fail to get the commit for a deleted reference.
-		// Set ref to empty string to fall back to the default branch.
-		ref = ""
+	if ref != input.Repo.DefaultBranch && actions_module.IsDefaultBranchWorkflow(input.Event) {
+		if ref != "" {
+			log.Warn("Event %q should only trigger workflows on the default branch, but its ref is %q. Will fall back to the default branch",
+				input.Event, ref)
+		}
+		ref = input.Repo.DefaultBranch
 	}
 	if ref == "" {
+		log.Warn("Ref of event %q is empty, will fall back to the default branch", input.Event)
 		ref = input.Repo.DefaultBranch
 	}
 
@@ -151,16 +157,17 @@ func notify(ctx context.Context, input *notifyInput) error {
 		return fmt.Errorf("gitRepo.GetCommit: %w", err)
 	}
 
-	if skipWorkflowsForCommit(input, commit) {
+	if skipWorkflows(input, commit) {
 		return nil
 	}
 
 	var detectedWorkflows []*actions_module.DetectedWorkflow
 	actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
+	shouldDetectSchedules := input.Event == webhook_module.HookEventPush && git.RefName(input.Ref).BranchName() == input.Repo.DefaultBranch
 	workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit,
 		input.Event,
 		input.Payload,
-		input.Event == webhook_module.HookEventPush && git.RefName(input.Ref).BranchName() == input.Repo.DefaultBranch,
+		shouldDetectSchedules,
 	)
 	if err != nil {
 		return fmt.Errorf("DetectWorkflows: %w", err)
@@ -207,15 +214,17 @@ func notify(ctx context.Context, input *notifyInput) error {
 		}
 	}
 
-	if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil {
-		return err
+	if shouldDetectSchedules {
+		if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil {
+			return err
+		}
 	}
 
 	return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)
 }
 
-func skipWorkflowsForCommit(input *notifyInput, commit *git.Commit) bool {
-	// skip workflow runs with a configured skip-ci string in commit message if the event is push or pull_request(_sync)
+func skipWorkflows(input *notifyInput, commit *git.Commit) bool {
+	// skip workflow runs with a configured skip-ci string in commit message or pr title if the event is push or pull_request(_sync)
 	// https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs
 	skipWorkflowEvents := []webhook_module.HookEventType{
 		webhook_module.HookEventPush,
@@ -224,6 +233,10 @@ func skipWorkflowsForCommit(input *notifyInput, commit *git.Commit) bool {
 	}
 	if slices.Contains(skipWorkflowEvents, input.Event) {
 		for _, s := range setting.Actions.SkipWorkflowStrings {
+			if input.PullRequest != nil && strings.Contains(input.PullRequest.Issue.Title, s) {
+				log.Debug("repo %s: skipped run for pr %v because of %s string", input.Repo.RepoPath(), input.PullRequest.Issue.ID, s)
+				return true
+			}
 			if strings.Contains(commit.CommitMessage, s) {
 				log.Debug("repo %s with commit %s: skipped run because of %s string", input.Repo.RepoPath(), commit.ID, s)
 				return true
@@ -287,23 +300,34 @@ func handleWorkflows(
 			run.NeedApproval = need
 		}
 
-		jobs, err := jobparser.Parse(dwf.Content)
+		if err := run.LoadAttributes(ctx); err != nil {
+			log.Error("LoadAttributes: %v", err)
+			continue
+		}
+
+		vars, err := actions_model.GetVariablesOfRun(ctx, run)
+		if err != nil {
+			log.Error("GetVariablesOfRun: %v", err)
+			continue
+		}
+
+		jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars))
 		if err != nil {
 			log.Error("jobparser.Parse: %v", err)
 			continue
 		}
 
-		// cancel running jobs if the event is push
-		if run.Event == webhook_module.HookEventPush {
-			// cancel running jobs of the same workflow
-			if err := actions_model.CancelRunningJobs(
+		// cancel running jobs if the event is push or pull_request_sync
+		if run.Event == webhook_module.HookEventPush ||
+			run.Event == webhook_module.HookEventPullRequestSync {
+			if err := actions_model.CancelPreviousJobs(
 				ctx,
 				run.RepoID,
 				run.Ref,
 				run.WorkflowID,
 				run.Event,
 			); err != nil {
-				log.Error("CancelRunningJobs: %v", err)
+				log.Error("CancelPreviousJobs: %v", err)
 			}
 		}
 
@@ -477,6 +501,10 @@ func handleSchedules(
 
 // DetectAndHandleSchedules detects the schedule workflows on the default branch and create schedule tasks
 func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) error {
+	if repo.IsEmpty || repo.IsArchived {
+		return nil
+	}
+
 	gitRepo, err := gitrepo.OpenRepository(context.Background(), repo)
 	if err != nil {
 		return fmt.Errorf("git.OpenRepository: %w", err)
@@ -497,12 +525,9 @@ func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository)
 	}
 
 	// We need a notifyInput to call handleSchedules
-	// Here we use the commit author as the Doer of the notifyInput
-	commitUser, err := user_model.GetUserByEmail(ctx, commit.Author.Email)
-	if err != nil {
-		return fmt.Errorf("get user by email: %w", err)
-	}
-	notifyInput := newNotifyInput(repo, commitUser, webhook_module.HookEventSchedule)
+	// if repo is a mirror, commit author maybe an external user,
+	// so we use action user as the Doer of the notifyInput
+	notifyInput := newNotifyInput(repo, user_model.NewActionsUser(), webhook_module.HookEventSchedule)
 
 	return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, repo.DefaultBranch)
 }
diff --git a/services/actions/rerun.go b/services/actions/rerun.go
new file mode 100644
index 0000000000..60f6650905
--- /dev/null
+++ b/services/actions/rerun.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/modules/container"
+)
+
+// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun
+func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob {
+	rerunJobs := []*actions_model.ActionRunJob{job}
+	rerunJobsIDSet := make(container.Set[string])
+	rerunJobsIDSet.Add(job.JobID)
+
+	for {
+		found := false
+		for _, j := range allJobs {
+			if rerunJobsIDSet.Contains(j.JobID) {
+				continue
+			}
+			for _, need := range j.Needs {
+				if rerunJobsIDSet.Contains(need) {
+					found = true
+					rerunJobs = append(rerunJobs, j)
+					rerunJobsIDSet.Add(j.JobID)
+					break
+				}
+			}
+		}
+		if !found {
+			break
+		}
+	}
+
+	return rerunJobs
+}
diff --git a/services/actions/rerun_test.go b/services/actions/rerun_test.go
new file mode 100644
index 0000000000..a98de7b788
--- /dev/null
+++ b/services/actions/rerun_test.go
@@ -0,0 +1,48 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+	"testing"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetAllRerunJobs(t *testing.T) {
+	job1 := &actions_model.ActionRunJob{JobID: "job1"}
+	job2 := &actions_model.ActionRunJob{JobID: "job2", Needs: []string{"job1"}}
+	job3 := &actions_model.ActionRunJob{JobID: "job3", Needs: []string{"job2"}}
+	job4 := &actions_model.ActionRunJob{JobID: "job4", Needs: []string{"job2", "job3"}}
+
+	jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4}
+
+	testCases := []struct {
+		job       *actions_model.ActionRunJob
+		rerunJobs []*actions_model.ActionRunJob
+	}{
+		{
+			job1,
+			[]*actions_model.ActionRunJob{job1, job2, job3, job4},
+		},
+		{
+			job2,
+			[]*actions_model.ActionRunJob{job2, job3, job4},
+		},
+		{
+			job3,
+			[]*actions_model.ActionRunJob{job3, job4},
+		},
+		{
+			job4,
+			[]*actions_model.ActionRunJob{job4},
+		},
+	}
+
+	for _, tc := range testCases {
+		rerunJobs := GetAllRerunJobs(tc.job, jobs)
+		assert.ElementsMatch(t, tc.rerunJobs, rerunJobs)
+	}
+}
diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go
index 79dd84e0cc..e4e56e5122 100644
--- a/services/actions/schedule_tasks.go
+++ b/services/actions/schedule_tasks.go
@@ -55,17 +55,22 @@ func startTasks(ctx context.Context) error {
 			// cancel running jobs if the event is push
 			if row.Schedule.Event == webhook_module.HookEventPush {
 				// cancel running jobs of the same workflow
-				if err := actions_model.CancelRunningJobs(
+				if err := actions_model.CancelPreviousJobs(
 					ctx,
 					row.RepoID,
 					row.Schedule.Ref,
 					row.Schedule.WorkflowID,
 					webhook_module.HookEventSchedule,
 				); err != nil {
-					log.Error("CancelRunningJobs: %v", err)
+					log.Error("CancelPreviousJobs: %v", err)
 				}
 			}
 
+			if row.Repo.IsArchived {
+				// Skip if the repo is archived
+				continue
+			}
+
 			cfg, err := row.Repo.GetUnit(ctx, unit.TypeActions)
 			if err != nil {
 				if repo_model.IsErrUnitTypeNotExist(err) {
diff --git a/services/actions/variables.go b/services/actions/variables.go
new file mode 100644
index 0000000000..8dde9c4af5
--- /dev/null
+++ b/services/actions/variables.go
@@ -0,0 +1,100 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+	"context"
+	"regexp"
+	"strings"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
+	secret_service "code.gitea.io/gitea/services/secrets"
+)
+
+func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) {
+	if err := secret_service.ValidateName(name); err != nil {
+		return nil, err
+	}
+
+	if err := envNameCIRegexMatch(name); err != nil {
+		return nil, err
+	}
+
+	v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data))
+	if err != nil {
+		return nil, err
+	}
+
+	return v, nil
+}
+
+func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) {
+	if err := secret_service.ValidateName(name); err != nil {
+		return false, err
+	}
+
+	if err := envNameCIRegexMatch(name); err != nil {
+		return false, err
+	}
+
+	return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
+		ID:   variableID,
+		Name: strings.ToUpper(name),
+		Data: util.ReserveLineBreakForTextarea(data),
+	})
+}
+
+func DeleteVariableByID(ctx context.Context, variableID int64) error {
+	return actions_model.DeleteVariable(ctx, variableID)
+}
+
+func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error {
+	if err := secret_service.ValidateName(name); err != nil {
+		return err
+	}
+
+	if err := envNameCIRegexMatch(name); err != nil {
+		return err
+	}
+
+	v, err := GetVariable(ctx, actions_model.FindVariablesOpts{
+		OwnerID: ownerID,
+		RepoID:  repoID,
+		Name:    name,
+	})
+	if err != nil {
+		return err
+	}
+
+	return actions_model.DeleteVariable(ctx, v.ID)
+}
+
+func GetVariable(ctx context.Context, opts actions_model.FindVariablesOpts) (*actions_model.ActionVariable, error) {
+	vars, err := actions_model.FindVariables(ctx, opts)
+	if err != nil {
+		return nil, err
+	}
+	if len(vars) != 1 {
+		return nil, util.NewNotExistErrorf("variable not found")
+	}
+	return vars[0], nil
+}
+
+// some regular expression of `variables` and `secrets`
+// reference to:
+// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
+// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
+var (
+	forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
+)
+
+func envNameCIRegexMatch(name string) error {
+	if forbiddenEnvNameCIRx.MatchString(name) {
+		log.Error("Env Name cannot be ci")
+		return util.NewInvalidArgumentErrorf("env name cannot be ci")
+	}
+	return nil
+}
diff --git a/services/agit/agit.go b/services/agit/agit.go
index bc68372570..52a70469e0 100644
--- a/services/agit/agit.go
+++ b/services/agit/agit.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"strconv"
 	"strings"
 
 	issues_model "code.gitea.io/gitea/models/issues"
@@ -15,28 +16,25 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/private"
+	"code.gitea.io/gitea/modules/setting"
 	notify_service "code.gitea.io/gitea/services/notify"
 	pull_service "code.gitea.io/gitea/services/pull"
 )
 
 // ProcReceive handle proc receive work
 func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) {
-	// TODO: Add more options?
-	var (
-		topicBranch string
-		title       string
-		description string
-		forcePush   bool
-	)
-
 	results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
+	topicBranch := opts.GitPushOptions["topic"]
+	forcePush, _ := strconv.ParseBool(opts.GitPushOptions["force-push"])
+	title := strings.TrimSpace(opts.GitPushOptions["title"])
+	description := strings.TrimSpace(opts.GitPushOptions["description"]) // TODO: Add more options?
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+	userName := strings.ToLower(opts.UserName)
 
-	ownerName := repo.OwnerName
-	repoName := repo.Name
-
-	topicBranch = opts.GitPushOptions["topic"]
-	_, forcePush = opts.GitPushOptions["force-push"]
-	objectFormat, _ := gitRepo.GetObjectFormat()
+	pusher, err := user_model.GetUserByID(ctx, opts.UserID)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get user. Error: %w", err)
+	}
 
 	for i := range opts.OldCommitIDs {
 		if opts.NewCommitIDs[i] == objectFormat.EmptyObjectID().String() {
@@ -80,9 +78,6 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 			continue
 		}
 
-		var headBranch string
-		userName := strings.ToLower(opts.UserName)
-
 		if len(curentTopicBranch) == 0 {
 			curentTopicBranch = topicBranch
 		}
@@ -90,6 +85,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 		// because different user maybe want to use same topic,
 		// So it's better to make sure the topic branch name
 		// has user name prefix
+		var headBranch string
 		if !strings.HasPrefix(curentTopicBranch, userName+"/") {
 			headBranch = userName + "/" + curentTopicBranch
 		} else {
@@ -99,26 +95,26 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 		pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit)
 		if err != nil {
 			if !issues_model.IsErrPullRequestNotExist(err) {
-				return nil, fmt.Errorf("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %w", ownerName, repoName, err)
+				return nil, fmt.Errorf("failed to get unmerged agit flow pull request in repository: %s Error: %w", repo.FullName(), err)
+			}
+
+			var commit *git.Commit
+			if title == "" || description == "" {
+				commit, err = gitRepo.GetCommit(opts.NewCommitIDs[i])
+				if err != nil {
+					return nil, fmt.Errorf("failed to get commit %s in repository: %s Error: %w", opts.NewCommitIDs[i], repo.FullName(), err)
+				}
 			}
 
 			// create a new pull request
-			if len(title) == 0 {
-				var has bool
-				title, has = opts.GitPushOptions["title"]
-				if !has || len(title) == 0 {
-					commit, err := gitRepo.GetCommit(opts.NewCommitIDs[i])
-					if err != nil {
-						return nil, fmt.Errorf("Failed to get commit %s in repository: %s/%s Error: %w", opts.NewCommitIDs[i], ownerName, repoName, err)
-					}
-					title = strings.Split(commit.CommitMessage, "\n")[0]
-				}
-				description = opts.GitPushOptions["description"]
+			if title == "" {
+				title = strings.Split(commit.CommitMessage, "\n")[0]
 			}
-
-			pusher, err := user_model.GetUserByID(ctx, opts.UserID)
-			if err != nil {
-				return nil, fmt.Errorf("Failed to get user. Error: %w", err)
+			if description == "" {
+				_, description, _ = strings.Cut(commit.CommitMessage, "\n\n")
+			}
+			if description == "" {
+				description = title
 			}
 
 			prIssue := &issues_model.Issue{
@@ -149,24 +145,27 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 
 			log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
 
-			objectFormat, _ := gitRepo.GetObjectFormat()
 			results = append(results, private.HookProcReceiveRefResult{
-				Ref:         pr.GetGitRefName(),
-				OriginalRef: opts.RefFullNames[i],
-				OldOID:      objectFormat.EmptyObjectID().String(),
-				NewOID:      opts.NewCommitIDs[i],
+				Ref:               pr.GetGitRefName(),
+				OriginalRef:       opts.RefFullNames[i],
+				OldOID:            objectFormat.EmptyObjectID().String(),
+				NewOID:            opts.NewCommitIDs[i],
+				IsCreatePR:        true,
+				URL:               fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
+				ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
+				HeadBranch:        headBranch,
 			})
 			continue
 		}
 
 		// update exist pull request
 		if err := pr.LoadBaseRepo(ctx); err != nil {
-			return nil, fmt.Errorf("Unable to load base repository for PR[%d] Error: %w", pr.ID, err)
+			return nil, fmt.Errorf("unable to load base repository for PR[%d] Error: %w", pr.ID, err)
 		}
 
 		oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
 		if err != nil {
-			return nil, fmt.Errorf("Unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err)
+			return nil, fmt.Errorf("unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err)
 		}
 
 		if oldCommitID == opts.NewCommitIDs[i] {
@@ -180,9 +179,11 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 		}
 
 		if !forcePush {
-			output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()})
+			output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").
+				AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).
+				RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()})
 			if err != nil {
-				return nil, fmt.Errorf("Fail to detect force push: %w", err)
+				return nil, fmt.Errorf("failed to detect force push: %w", err)
 			} else if len(output) > 0 {
 				results = append(results, private.HookProcReceiveRefResult{
 					OriginalRef: opts.RefFullNames[i],
@@ -196,17 +197,13 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 
 		pr.HeadCommitID = opts.NewCommitIDs[i]
 		if err = pull_service.UpdateRef(ctx, pr); err != nil {
-			return nil, fmt.Errorf("Failed to update pull ref. Error: %w", err)
+			return nil, fmt.Errorf("failed to update pull ref. Error: %w", err)
 		}
 
 		pull_service.AddToTaskQueue(ctx, pr)
-		pusher, err := user_model.GetUserByID(ctx, opts.UserID)
-		if err != nil {
-			return nil, fmt.Errorf("Failed to get user. Error: %w", err)
-		}
 		err = pr.LoadIssue(ctx)
 		if err != nil {
-			return nil, fmt.Errorf("Failed to load pull issue. Error: %w", err)
+			return nil, fmt.Errorf("failed to load pull issue. Error: %w", err)
 		}
 		comment, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i])
 		if err == nil && comment != nil {
@@ -216,11 +213,14 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 		isForcePush := comment != nil && comment.IsForcePush
 
 		results = append(results, private.HookProcReceiveRefResult{
-			OldOID:      oldCommitID,
-			NewOID:      opts.NewCommitIDs[i],
-			Ref:         pr.GetGitRefName(),
-			OriginalRef: opts.RefFullNames[i],
-			IsForcePush: isForcePush,
+			OldOID:            oldCommitID,
+			NewOID:            opts.NewCommitIDs[i],
+			Ref:               pr.GetGitRefName(),
+			OriginalRef:       opts.RefFullNames[i],
+			IsForcePush:       isForcePush,
+			IsCreatePR:        false,
+			URL:               fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
+			ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
 		})
 	}
 
diff --git a/services/asymkey/deploy_key.go b/services/asymkey/deploy_key.go
index e127cbfc6e..324688c534 100644
--- a/services/asymkey/deploy_key.go
+++ b/services/asymkey/deploy_key.go
@@ -7,7 +7,6 @@ import (
 	"context"
 
 	"code.gitea.io/gitea/models"
-	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 )
@@ -27,5 +26,5 @@ func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error
 		return err
 	}
 
-	return asymkey_model.RewriteAllPublicKeys(ctx)
+	return RewriteAllPublicKeys(ctx)
 }
diff --git a/services/asymkey/ssh_key.go b/services/asymkey/ssh_key.go
index 83d7edafa3..da57059d4b 100644
--- a/services/asymkey/ssh_key.go
+++ b/services/asymkey/ssh_key.go
@@ -43,8 +43,8 @@ func DeletePublicKey(ctx context.Context, doer *user_model.User, id int64) (err
 	committer.Close()
 
 	if key.Type == asymkey_model.KeyTypePrincipal {
-		return asymkey_model.RewriteAllPrincipalKeys(ctx)
+		return RewriteAllPrincipalKeys(ctx)
 	}
 
-	return asymkey_model.RewriteAllPublicKeys(ctx)
+	return RewriteAllPublicKeys(ctx)
 }
diff --git a/services/asymkey/ssh_key_authorized_keys.go b/services/asymkey/ssh_key_authorized_keys.go
new file mode 100644
index 0000000000..5caa5bbfb6
--- /dev/null
+++ b/services/asymkey/ssh_key_authorized_keys.go
@@ -0,0 +1,79 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"time"
+
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
+)
+
+// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
+// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
+// outside any session scope independently.
+func RewriteAllPublicKeys(ctx context.Context) error {
+	// Don't rewrite key if internal server
+	if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
+		return nil
+	}
+
+	return asymkey_model.WithSSHOpLocker(func() error {
+		return rewriteAllPublicKeys(ctx)
+	})
+}
+
+func rewriteAllPublicKeys(ctx context.Context) error {
+	if setting.SSH.RootPath != "" {
+		// First of ensure that the RootPath is present, and if not make it with 0700 permissions
+		// This of course doesn't guarantee that this is the right directory for authorized_keys
+		// but at least if it's supposed to be this directory and it doesn't exist and we're the
+		// right user it will at least be created properly.
+		err := os.MkdirAll(setting.SSH.RootPath, 0o700)
+		if err != nil {
+			log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
+			return err
+		}
+	}
+
+	fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
+	tmpPath := fPath + ".tmp"
+	t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		t.Close()
+		if err := util.Remove(tmpPath); err != nil {
+			log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err)
+		}
+	}()
+
+	if setting.SSH.AuthorizedKeysBackup {
+		isExist, err := util.IsExist(fPath)
+		if err != nil {
+			log.Error("Unable to check if %s exists. Error: %v", fPath, err)
+			return err
+		}
+		if isExist {
+			bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
+			if err = util.CopyFile(fPath, bakPath); err != nil {
+				return err
+			}
+		}
+	}
+
+	if err := asymkey_model.RegeneratePublicKeys(ctx, t); err != nil {
+		return err
+	}
+
+	t.Close()
+	return util.Rename(tmpPath, fPath)
+}
diff --git a/models/asymkey/ssh_key_authorized_principals.go b/services/asymkey/ssh_key_authorized_principals.go
similarity index 72%
rename from models/asymkey/ssh_key_authorized_principals.go
rename to services/asymkey/ssh_key_authorized_principals.go
index 107d70c766..2838bb5fc7 100644
--- a/models/asymkey/ssh_key_authorized_principals.go
+++ b/services/asymkey/ssh_key_authorized_principals.go
@@ -13,31 +13,22 @@ import (
 	"strings"
 	"time"
 
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 )
 
-//  _____          __  .__                 .__                  .___
-// /  _  \  __ ___/  |_|  |__   ___________|__|_______ ____   __| _/
-// /  /_\  \|  |  \   __\  |  \ /  _ \_  __ \  \___   // __ \ / __ |
-// /    |    \  |  /|  | |   Y  (  <_> )  | \/  |/    /\  ___// /_/ |
-// \____|__  /____/ |__| |___|  /\____/|__|  |__/_____ \\___  >____ |
-//         \/                 \/                      \/    \/     \/
-// __________       .__              .__             .__
-// \______   _______|__| ____   ____ |_____________  |  |   ______
-//  |     ___\_  __ |  |/    \_/ ___\|  \____ \__  \ |  |  /  ___/
-//  |    |    |  | \|  |   |  \  \___|  |  |_> / __ \|  |__\___ \
-//  |____|    |__|  |__|___|  /\___  |__|   __(____  |____/____  >
-//                          \/     \/   |__|       \/          \/
-//
 // This file contains functions for creating authorized_principals files
 //
 // There is a dependence on the database within RewriteAllPrincipalKeys & RegeneratePrincipalKeys
 // The sshOpLocker is used from ssh_key_authorized_keys.go
 
-const authorizedPrincipalsFile = "authorized_principals"
+const (
+	authorizedPrincipalsFile = "authorized_principals"
+	tplCommentPrefix         = `# gitea public key`
+)
 
 // RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again.
 // Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
@@ -48,9 +39,12 @@ func RewriteAllPrincipalKeys(ctx context.Context) error {
 		return nil
 	}
 
-	sshOpLocker.Lock()
-	defer sshOpLocker.Unlock()
+	return asymkey_model.WithSSHOpLocker(func() error {
+		return rewriteAllPrincipalKeys(ctx)
+	})
+}
 
+func rewriteAllPrincipalKeys(ctx context.Context) error {
 	if setting.SSH.RootPath != "" {
 		// First of ensure that the RootPath is present, and if not make it with 0700 permissions
 		// This of course doesn't guarantee that this is the right directory for authorized_keys
@@ -97,8 +91,8 @@ func RewriteAllPrincipalKeys(ctx context.Context) error {
 }
 
 func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
-	if err := db.GetEngine(ctx).Where("type = ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
-		_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
+	if err := db.GetEngine(ctx).Where("type = ?", asymkey_model.KeyTypePrincipal).Iterate(new(asymkey_model.PublicKey), func(idx int, bean any) (err error) {
+		_, err = t.WriteString((bean.(*asymkey_model.PublicKey)).AuthorizedString())
 		return err
 	}); err != nil {
 		return err
@@ -115,6 +109,8 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
 		if err != nil {
 			return err
 		}
+		defer f.Close()
+
 		scanner := bufio.NewScanner(f)
 		for scanner.Scan() {
 			line := scanner.Text()
@@ -124,11 +120,12 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
 			}
 			_, err = t.WriteString(line + "\n")
 			if err != nil {
-				f.Close()
 				return err
 			}
 		}
-		f.Close()
+		if err = scanner.Err(); err != nil {
+			return fmt.Errorf("regeneratePrincipalKeys scan: %w", err)
+		}
 	}
 	return nil
 }
diff --git a/services/asymkey/ssh_key_principals.go b/services/asymkey/ssh_key_principals.go
new file mode 100644
index 0000000000..5ed5cfa782
--- /dev/null
+++ b/services/asymkey/ssh_key_principals.go
@@ -0,0 +1,54 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+	"context"
+	"fmt"
+
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/perm"
+)
+
+// AddPrincipalKey adds new principal to database and authorized_principals file.
+func AddPrincipalKey(ctx context.Context, ownerID int64, content string, authSourceID int64) (*asymkey_model.PublicKey, error) {
+	dbCtx, committer, err := db.TxContext(ctx)
+	if err != nil {
+		return nil, err
+	}
+	defer committer.Close()
+
+	// Principals cannot be duplicated.
+	has, err := db.GetEngine(dbCtx).
+		Where("content = ? AND type = ?", content, asymkey_model.KeyTypePrincipal).
+		Get(new(asymkey_model.PublicKey))
+	if err != nil {
+		return nil, err
+	} else if has {
+		return nil, asymkey_model.ErrKeyAlreadyExist{
+			Content: content,
+		}
+	}
+
+	key := &asymkey_model.PublicKey{
+		OwnerID:       ownerID,
+		Name:          content,
+		Content:       content,
+		Mode:          perm.AccessModeWrite,
+		Type:          asymkey_model.KeyTypePrincipal,
+		LoginSourceID: authSourceID,
+	}
+	if err = db.Insert(dbCtx, key); err != nil {
+		return nil, fmt.Errorf("addKey: %w", err)
+	}
+
+	if err = committer.Commit(); err != nil {
+		return nil, err
+	}
+
+	committer.Close()
+
+	return key, RewriteAllPrincipalKeys(ctx)
+}
diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go
index 967332fd98..0fd51e4fa5 100644
--- a/services/attachment/attachment.go
+++ b/services/attachment/attachment.go
@@ -12,8 +12,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/storage"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/services/context/upload"
 
 	"github.com/google/uuid"
 )
@@ -39,14 +39,14 @@ func NewAttachment(ctx context.Context, attach *repo_model.Attachment, file io.R
 }
 
 // UploadAttachment upload new attachment into storage and update database
-func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, opts *repo_model.Attachment) (*repo_model.Attachment, error) {
+func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
 	buf := make([]byte, 1024)
 	n, _ := util.ReadAtMost(file, buf)
 	buf = buf[:n]
 
-	if err := upload.Verify(buf, opts.Name, allowedTypes); err != nil {
+	if err := upload.Verify(buf, attach.Name, allowedTypes); err != nil {
 		return nil, err
 	}
 
-	return NewAttachment(ctx, opts, io.MultiReader(bytes.NewReader(buf), file), fileSize)
+	return NewAttachment(ctx, attach, io.MultiReader(bytes.NewReader(buf), file), fileSize)
 }
diff --git a/services/auth/auth.go b/services/auth/auth.go
index 6746dc2a54..a2523a2452 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -12,12 +12,12 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/webauthn"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/session"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web/middleware"
+	gitea_context "code.gitea.io/gitea/services/context"
 	user_service "code.gitea.io/gitea/services/user"
 )
 
@@ -40,6 +40,7 @@ func isContainerPath(req *http.Request) bool {
 var (
 	gitRawOrAttachPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`)
 	lfsPathRe            = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`)
+	archivePathRe        = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/archive/`)
 )
 
 func isGitRawOrAttachPath(req *http.Request) bool {
@@ -56,6 +57,10 @@ func isGitRawOrAttachOrLFSPath(req *http.Request) bool {
 	return false
 }
 
+func isArchivePath(req *http.Request) bool {
+	return archivePathRe.MatchString(req.URL.Path)
+}
+
 // handleSignIn clears existing session variables and stores new ones for the specified user object
 func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *user_model.User) {
 	// We need to regenerate the session...
diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go
index f2f7858a85..46d8510143 100644
--- a/services/auth/oauth2.go
+++ b/services/auth/oauth2.go
@@ -133,7 +133,7 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat
 func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
 	// These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs
 	if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) &&
-		!isGitRawOrAttachPath(req) {
+		!isGitRawOrAttachPath(req) && !isArchivePath(req) {
 		return nil, nil
 	}
 
diff --git a/services/auth/reverseproxy.go b/services/auth/reverseproxy.go
index 359c1f2473..b6aeb0aed2 100644
--- a/services/auth/reverseproxy.go
+++ b/services/auth/reverseproxy.go
@@ -10,8 +10,8 @@ import (
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web/middleware"
 
 	gouuid "github.com/google/uuid"
@@ -161,7 +161,7 @@ func (r *ReverseProxy) newUser(req *http.Request) *user_model.User {
 	}
 
 	overwriteDefault := user_model.CreateUserOverwriteOptions{
-		IsActive: util.OptionalBoolTrue,
+		IsActive: optional.Some(true),
 	}
 
 	if err := user_model.CreateUser(req.Context(), user, &overwriteDefault); err != nil {
diff --git a/services/auth/session.go b/services/auth/session.go
index d13813dcbe..35d97e42da 100644
--- a/services/auth/session.go
+++ b/services/auth/session.go
@@ -4,7 +4,6 @@
 package auth
 
 import (
-	"context"
 	"net/http"
 
 	user_model "code.gitea.io/gitea/models/user"
@@ -29,40 +28,33 @@ func (s *Session) Name() string {
 // object for that uid.
 // Returns nil if there is no user uid stored in the session.
 func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
-	user := SessionUser(req.Context(), sess)
-	if user != nil {
-		return user, nil
-	}
-	return nil, nil
-}
-
-// SessionUser returns the user object corresponding to the "uid" session variable.
-func SessionUser(ctx context.Context, sess SessionStore) *user_model.User {
 	if sess == nil {
-		return nil
+		return nil, nil
 	}
 
 	// Get user ID
 	uid := sess.Get("uid")
 	if uid == nil {
-		return nil
+		return nil, nil
 	}
 	log.Trace("Session Authorization: Found user[%d]", uid)
 
 	id, ok := uid.(int64)
 	if !ok {
-		return nil
+		return nil, nil
 	}
 
 	// Get user object
-	user, err := user_model.GetUserByID(ctx, id)
+	user, err := user_model.GetUserByID(req.Context(), id)
 	if err != nil {
 		if !user_model.IsErrUserNotExist(err) {
-			log.Error("GetUserById: %v", err)
+			log.Error("GetUserByID: %v", err)
+			// Return the err as-is to keep current signed-in session, in case the err is something like context.Canceled. Otherwise non-existing user (nil, nil) will make the caller clear the signed-in session.
+			return nil, err
 		}
-		return nil
+		return nil, nil
 	}
 
 	log.Trace("Session Authorization: Logged in user %-v", user)
-	return user
+	return user, nil
 }
diff --git a/services/auth/signin.go b/services/auth/signin.go
index fafe3ef3c6..e116a088e0 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -11,7 +11,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
 	"code.gitea.io/gitea/services/auth/source/smtp"
 
@@ -87,7 +87,7 @@ func UserSignIn(ctx context.Context, username, password string) (*user_model.Use
 	}
 
 	sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
-		IsActive: util.OptionalBoolTrue,
+		IsActive: optional.Some(true),
 	})
 	if err != nil {
 		return nil, nil, err
diff --git a/services/auth/source/db/source.go b/services/auth/source/db/source.go
index 50eae27439..bb2270cbd6 100644
--- a/services/auth/source/db/source.go
+++ b/services/auth/source/db/source.go
@@ -18,7 +18,7 @@ func (source *Source) FromDB(bs []byte) error {
 	return nil
 }
 
-// ToDB exports an SMTPConfig to a serialized format.
+// ToDB exports the config to a byte slice to be saved into database (this method is just dummy and does nothing for DB source)
 func (source *Source) ToDB() ([]byte, error) {
 	return nil, nil
 }
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 8f641ed541..6ebd3ea50a 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -13,7 +13,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	auth_module "code.gitea.io/gitea/modules/auth"
 	"code.gitea.io/gitea/modules/optional"
-	"code.gitea.io/gitea/modules/util"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	source_service "code.gitea.io/gitea/services/auth/source"
 	user_service "code.gitea.io/gitea/services/user"
 )
@@ -69,7 +69,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 
 	if user != nil {
 		if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, user, source.authSource, sr.SSHPublicKey) {
-			if err := asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+			if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
 				return user, err
 			}
 		}
@@ -85,8 +85,8 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 			IsAdmin:     sr.IsAdmin,
 		}
 		overwriteDefault := &user_model.CreateUserOverwriteOptions{
-			IsRestricted: util.OptionalBoolOf(sr.IsRestricted),
-			IsActive:     util.OptionalBoolTrue,
+			IsRestricted: optional.Some(sr.IsRestricted),
+			IsActive:     optional.Some(true),
 		}
 
 		err := user_model.CreateUser(ctx, user, overwriteDefault)
@@ -95,7 +95,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 		}
 
 		if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.authSource, sr.SSHPublicKey) {
-			if err := asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+			if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
 				return user, err
 			}
 		}
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index eee7bb585a..0c9491cd09 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -16,7 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
-	"code.gitea.io/gitea/modules/util"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	source_service "code.gitea.io/gitea/services/auth/source"
 	user_service "code.gitea.io/gitea/services/user"
 )
@@ -78,7 +78,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 			log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.authSource.Name)
 			// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
 			if sshKeysNeedUpdate {
-				err = asymkey_model.RewriteAllPublicKeys(ctx)
+				err = asymkey_service.RewriteAllPublicKeys(ctx)
 				if err != nil {
 					log.Error("RewriteAllPublicKeys: %v", err)
 				}
@@ -125,8 +125,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 				IsAdmin:     su.IsAdmin,
 			}
 			overwriteDefault := &user_model.CreateUserOverwriteOptions{
-				IsRestricted: util.OptionalBoolOf(su.IsRestricted),
-				IsActive:     util.OptionalBoolTrue,
+				IsRestricted: optional.Some(su.IsRestricted),
+				IsActive:     optional.Some(true),
 			}
 
 			err = user_model.CreateUser(ctx, usr, overwriteDefault)
@@ -196,7 +196,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 
 	// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
 	if sshKeysNeedUpdate {
-		err = asymkey_model.RewriteAllPublicKeys(ctx)
+		err = asymkey_service.RewriteAllPublicKeys(ctx)
 		if err != nil {
 			log.Error("RewriteAllPublicKeys: %v", err)
 		}
diff --git a/services/auth/source/oauth2/init.go b/services/auth/source/oauth2/init.go
index 3ad6e307f1..5c25681548 100644
--- a/services/auth/source/oauth2/init.go
+++ b/services/auth/source/oauth2/init.go
@@ -12,8 +12,8 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/google/uuid"
 	"github.com/gorilla/sessions"
@@ -66,7 +66,7 @@ func ResetOAuth2(ctx context.Context) error {
 // initOAuth2Sources is used to load and register all active OAuth2 providers
 func initOAuth2Sources(ctx context.Context) error {
 	authSources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
-		IsActive:  util.OptionalBoolTrue,
+		IsActive:  optional.Some(true),
 		LoginType: auth.OAuth2,
 	})
 	if err != nil {
diff --git a/services/auth/source/oauth2/jwtsigningkey.go b/services/auth/source/oauth2/jwtsigningkey.go
index eca0b8b7e1..070fffe60f 100644
--- a/services/auth/source/oauth2/jwtsigningkey.go
+++ b/services/auth/source/oauth2/jwtsigningkey.go
@@ -300,7 +300,7 @@ func InitSigningKey() error {
 	case "HS384":
 		fallthrough
 	case "HS512":
-		key, err = loadSymmetricKey()
+		key = setting.GetGeneralTokenSigningSecret()
 	case "RS256":
 		fallthrough
 	case "RS384":
@@ -333,12 +333,6 @@ func InitSigningKey() error {
 	return nil
 }
 
-// loadSymmetricKey checks if the configured secret is valid.
-// If it is not valid, it will return an error.
-func loadSymmetricKey() (any, error) {
-	return util.Base64FixedDecode(base64.RawURLEncoding, []byte(setting.OAuth2.JWTSecretBase64), 32)
-}
-
 // loadOrCreateAsymmetricKey checks if the configured private key exists.
 // If it does not exist a new random key gets generated and saved on the configured path.
 func loadOrCreateAsymmetricKey() (any, error) {
diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go
index f4edb507f2..6ed6c184eb 100644
--- a/services/auth/source/oauth2/providers.go
+++ b/services/auth/source/oauth2/providers.go
@@ -15,8 +15,8 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/markbates/goth"
 )
@@ -59,7 +59,7 @@ func (p *AuthSourceProvider) DisplayName() string {
 
 func (p *AuthSourceProvider) IconHTML(size int) template.HTML {
 	if p.iconURL != "" {
-		img := fmt.Sprintf(`<img class="gt-object-contain gt-mr-3" width="%d" height="%d" src="%s" alt="%s">`,
+		img := fmt.Sprintf(`<img class="tw-object-contain tw-mr-2" width="%d" height="%d" src="%s" alt="%s">`,
 			size,
 			size,
 			html.EscapeString(p.iconURL), html.EscapeString(p.DisplayName()),
@@ -107,7 +107,7 @@ func CreateProviderFromSource(source *auth.Source) (Provider, error) {
 }
 
 // GetOAuth2Providers returns the list of configured OAuth2 providers
-func GetOAuth2Providers(ctx context.Context, isActive util.OptionalBool) ([]Provider, error) {
+func GetOAuth2Providers(ctx context.Context, isActive optional.Option[bool]) ([]Provider, error) {
 	authSources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
 		IsActive:  isActive,
 		LoginType: auth.OAuth2,
diff --git a/services/auth/source/oauth2/providers_base.go b/services/auth/source/oauth2/providers_base.go
index 5b6694487b..9d4ab106e5 100644
--- a/services/auth/source/oauth2/providers_base.go
+++ b/services/auth/source/oauth2/providers_base.go
@@ -35,10 +35,10 @@ func (b *BaseProvider) IconHTML(size int) template.HTML {
 	case "github":
 		svgName = "octicon-mark-github"
 	}
-	svgHTML := svg.RenderHTML(svgName, size, "gt-mr-3")
+	svgHTML := svg.RenderHTML(svgName, size, "tw-mr-2")
 	if svgHTML == "" {
 		log.Error("No SVG icon for oauth2 provider %q", b.name)
-		svgHTML = svg.RenderHTML("gitea-openid", size, "gt-mr-3")
+		svgHTML = svg.RenderHTML("gitea-openid", size, "tw-mr-2")
 	}
 	return svgHTML
 }
diff --git a/services/auth/source/oauth2/providers_openid.go b/services/auth/source/oauth2/providers_openid.go
index a4dcfcafc7..285876d5ac 100644
--- a/services/auth/source/oauth2/providers_openid.go
+++ b/services/auth/source/oauth2/providers_openid.go
@@ -29,7 +29,7 @@ func (o *OpenIDProvider) DisplayName() string {
 
 // IconHTML returns icon HTML for this provider
 func (o *OpenIDProvider) IconHTML(size int) template.HTML {
-	return svg.RenderHTML("gitea-openid", size, "gt-mr-3")
+	return svg.RenderHTML("gitea-openid", size, "tw-mr-2")
 }
 
 // CreateGothProvider creates a GothProvider from this Provider
diff --git a/services/auth/source/pam/source_authenticate.go b/services/auth/source/pam/source_authenticate.go
index 0891a86392..addd1bd2c9 100644
--- a/services/auth/source/pam/source_authenticate.go
+++ b/services/auth/source/pam/source_authenticate.go
@@ -11,8 +11,8 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/auth/pam"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/google/uuid"
 )
@@ -60,7 +60,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 		LoginName:   userName, // This is what the user typed in
 	}
 	overwriteDefault := &user_model.CreateUserOverwriteOptions{
-		IsActive: util.OptionalBoolTrue,
+		IsActive: optional.Some(true),
 	}
 
 	if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
diff --git a/services/auth/source/smtp/source_authenticate.go b/services/auth/source/smtp/source_authenticate.go
index b244fc7d40..1f0a61c789 100644
--- a/services/auth/source/smtp/source_authenticate.go
+++ b/services/auth/source/smtp/source_authenticate.go
@@ -12,6 +12,7 @@ import (
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/util"
 )
 
@@ -75,7 +76,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 		LoginName:   userName,
 	}
 	overwriteDefault := &user_model.CreateUserOverwriteOptions{
-		IsActive: util.OptionalBoolTrue,
+		IsActive: optional.Some(true),
 	}
 
 	if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
diff --git a/services/auth/source/source_group_sync.go b/services/auth/source/source_group_sync.go
index 3a2411ec55..05293f202f 100644
--- a/services/auth/source/source_group_sync.go
+++ b/services/auth/source/source_group_sync.go
@@ -100,12 +100,12 @@ func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeam
 			}
 
 			if action == syncAdd && !isMember {
-				if err := models.AddTeamMember(ctx, team, user.ID); err != nil {
+				if err := models.AddTeamMember(ctx, team, user); err != nil {
 					log.Error("group sync: Could not add user to team: %v", err)
 					return err
 				}
 			} else if action == syncRemove && isMember {
-				if err := models.RemoveTeamMember(ctx, team, user.ID); err != nil {
+				if err := models.RemoveTeamMember(ctx, team, user); err != nil {
 					log.Error("group sync: Could not remove user from team: %v", err)
 					return err
 				}
diff --git a/services/auth/sspi.go b/services/auth/sspi.go
index 0e974fde8f..64a127e97a 100644
--- a/services/auth/sspi.go
+++ b/services/auth/sspi.go
@@ -14,12 +14,12 @@ import (
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/services/auth/source/sspi"
+	gitea_context "code.gitea.io/gitea/services/context"
 
 	gouuid "github.com/google/uuid"
 )
@@ -131,7 +131,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
 // getConfig retrieves the SSPI configuration from login sources
 func (s *SSPI) getConfig(ctx context.Context) (*sspi.Source, error) {
 	sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
-		IsActive:  util.OptionalBoolTrue,
+		IsActive:  optional.Some(true),
 		LoginType: auth.SSPI,
 	})
 	if err != nil {
@@ -172,8 +172,8 @@ func (s *SSPI) newUser(ctx context.Context, username string, cfg *sspi.Source) (
 	}
 	emailNotificationPreference := user_model.EmailNotificationsDisabled
 	overwriteDefault := &user_model.CreateUserOverwriteOptions{
-		IsActive:                     util.OptionalBoolOf(cfg.AutoActivateUsers),
-		KeepEmailPrivate:             util.OptionalBoolTrue,
+		IsActive:                     optional.Some(cfg.AutoActivateUsers),
+		KeepEmailPrivate:             optional.Some(true),
 		EmailNotificationsPreference: &emailNotificationPreference,
 	}
 	if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
diff --git a/modules/context/access_log.go b/services/context/access_log.go
similarity index 100%
rename from modules/context/access_log.go
rename to services/context/access_log.go
diff --git a/modules/context/api.go b/services/context/api.go
similarity index 97%
rename from modules/context/api.go
rename to services/context/api.go
index e226264a87..b18a206b5e 100644
--- a/modules/context/api.go
+++ b/services/context/api.go
@@ -245,7 +245,7 @@ func APIContexter() func(http.Handler) http.Handler {
 // NotFound handles 404s for APIContext
 // String will replace message, errors will be added to a slice
 func (ctx *APIContext) NotFound(objs ...any) {
-	message := ctx.Tr("error.not_found")
+	message := ctx.Locale.TrString("error.not_found")
 	var errors []string
 	for _, obj := range objs {
 		// Ignore nil
@@ -307,12 +307,6 @@ func RepoRefForAPI(next http.Handler) http.Handler {
 			return
 		}
 
-		objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
-		if err != nil {
-			ctx.Error(http.StatusInternalServerError, "GetCommit", err)
-			return
-		}
-
 		if ref := ctx.FormTrim("ref"); len(ref) > 0 {
 			commit, err := ctx.Repo.GitRepo.GetCommit(ref)
 			if err != nil {
@@ -331,6 +325,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
 		}
 
 		refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny)
+		var err error
 
 		if ctx.Repo.GitRepo.IsBranchExist(refName) {
 			ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
@@ -346,7 +341,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
 				return
 			}
 			ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
-		} else if len(refName) == objectFormat.FullLength() {
+		} else if len(refName) == ctx.Repo.GetObjectFormat().FullLength() {
 			ctx.Repo.CommitID = refName
 			ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
 			if err != nil {
diff --git a/modules/context/api_org.go b/services/context/api_org.go
similarity index 100%
rename from modules/context/api_org.go
rename to services/context/api_org.go
diff --git a/modules/context/api_test.go b/services/context/api_test.go
similarity index 100%
rename from modules/context/api_test.go
rename to services/context/api_test.go
diff --git a/modules/context/base.go b/services/context/base.go
similarity index 89%
rename from modules/context/base.go
rename to services/context/base.go
index 8df1dde866..62fb743714 100644
--- a/modules/context/base.go
+++ b/services/context/base.go
@@ -6,6 +6,7 @@ package context
 import (
 	"context"
 	"fmt"
+	"html/template"
 	"io"
 	"net/http"
 	"net/url"
@@ -16,8 +17,8 @@ import (
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/translation"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web/middleware"
 
 	"github.com/go-chi/chi/v5"
@@ -206,17 +207,17 @@ func (b *Base) FormBool(key string) bool {
 	return v
 }
 
-// FormOptionalBool returns an OptionalBoolTrue or OptionalBoolFalse if the value
-// for the provided key exists in the form else it returns OptionalBoolNone
-func (b *Base) FormOptionalBool(key string) util.OptionalBool {
+// FormOptionalBool returns an optional.Some(true) or optional.Some(false) if the value
+// for the provided key exists in the form else it returns optional.None[bool]()
+func (b *Base) FormOptionalBool(key string) optional.Option[bool] {
 	value := b.Req.FormValue(key)
 	if len(value) == 0 {
-		return util.OptionalBoolNone
+		return optional.None[bool]()
 	}
 	s := b.Req.FormValue(key)
 	v, _ := strconv.ParseBool(s)
 	v = v || strings.EqualFold(s, "on")
-	return util.OptionalBoolOf(v)
+	return optional.Some(v)
 }
 
 func (b *Base) SetFormString(key, value string) {
@@ -255,7 +256,7 @@ func (b *Base) Redirect(location string, status ...int) {
 		code = status[0]
 	}
 
-	if strings.Contains(location, "://") || strings.HasPrefix(location, "//") {
+	if strings.HasPrefix(location, "http://") || strings.HasPrefix(location, "https://") || strings.HasPrefix(location, "//") {
 		// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
 		// 1. the first request to "/my-path" contains cookie
 		// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
@@ -264,6 +265,14 @@ func (b *Base) Redirect(location string, status ...int) {
 		// So in this case, we should remove the session cookie from the response header
 		removeSessionCookieHeader(b.Resp)
 	}
+	// in case the request is made by htmx, have it redirect the browser instead of trying to follow the redirect inside htmx
+	if b.Req.Header.Get("HX-Request") == "true" {
+		b.Resp.Header().Set("HX-Redirect", location)
+		// we have to return a non-redirect status code so XMLHTTPRequest will not immediately follow the redirect
+		// so as to give htmx redirect logic a chance to run
+		b.Status(http.StatusNoContent)
+		return
+	}
 	http.Redirect(b.Resp, b.Req, location, code)
 }
 
@@ -286,11 +295,11 @@ func (b *Base) cleanUp() {
 	}
 }
 
-func (b *Base) Tr(msg string, args ...any) string {
+func (b *Base) Tr(msg string, args ...any) template.HTML {
 	return b.Locale.Tr(msg, args...)
 }
 
-func (b *Base) TrN(cnt any, key1, keyN string, args ...any) string {
+func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
 	return b.Locale.TrN(cnt, key1, keyN, args...)
 }
 
diff --git a/services/context/base_test.go b/services/context/base_test.go
new file mode 100644
index 0000000000..823f20e00b
--- /dev/null
+++ b/services/context/base_test.go
@@ -0,0 +1,47 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package context
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRedirect(t *testing.T) {
+	req, _ := http.NewRequest("GET", "/", nil)
+
+	cases := []struct {
+		url  string
+		keep bool
+	}{
+		{"http://test", false},
+		{"https://test", false},
+		{"//test", false},
+		{"/://test", true},
+		{"/test", true},
+	}
+	for _, c := range cases {
+		resp := httptest.NewRecorder()
+		b, cleanup := NewBaseContext(resp, req)
+		resp.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "dummy"}).String())
+		b.Redirect(c.url)
+		cleanup()
+		has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy"
+		assert.Equal(t, c.keep, has, "url = %q", c.url)
+	}
+
+	req, _ = http.NewRequest("GET", "/", nil)
+	resp := httptest.NewRecorder()
+	req.Header.Add("HX-Request", "true")
+	b, cleanup := NewBaseContext(resp, req)
+	b.Redirect("/other")
+	cleanup()
+	assert.Equal(t, "/other", resp.Header().Get("HX-Redirect"))
+	assert.Equal(t, http.StatusNoContent, resp.Code)
+}
diff --git a/modules/context/captcha.go b/services/context/captcha.go
similarity index 95%
rename from modules/context/captcha.go
rename to services/context/captcha.go
index a1999900c9..fa8d779f56 100644
--- a/modules/context/captcha.go
+++ b/services/context/captcha.go
@@ -79,11 +79,11 @@ func VerifyCaptcha(ctx *Context, tpl base.TplName, form any) {
 	case setting.CfTurnstile:
 		valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField))
 	default:
-		ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
+		ctx.ServerError("Unknown Captcha Type", fmt.Errorf("unknown Captcha Type: %s", setting.Service.CaptchaType))
 		return
 	}
 	if err != nil {
-		log.Debug("%v", err)
+		log.Debug("Captcha Verify failed: %v", err)
 	}
 
 	if !valid {
diff --git a/modules/context/context.go b/services/context/context.go
similarity index 93%
rename from modules/context/context.go
rename to services/context/context.go
index d19c5d1198..4b318f7e33 100644
--- a/modules/context/context.go
+++ b/services/context/context.go
@@ -6,7 +6,8 @@ package context
 
 import (
 	"context"
-	"html"
+	"encoding/hex"
+	"fmt"
 	"html/template"
 	"io"
 	"net/http"
@@ -71,16 +72,6 @@ func init() {
 	})
 }
 
-// TrHTMLEscapeArgs runs ".Locale.Tr()" but pre-escapes all arguments with html.EscapeString.
-// This is useful if the locale message is intended to only produce HTML content.
-func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string {
-	trArgs := make([]any, len(args))
-	for i, arg := range args {
-		trArgs[i] = html.EscapeString(arg)
-	}
-	return ctx.Locale.Tr(msg, trArgs...)
-}
-
 type webContextKeyType struct{}
 
 var WebContextKey = webContextKeyType{}
@@ -134,7 +125,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
 func Contexter() func(next http.Handler) http.Handler {
 	rnd := templates.HTMLRenderer()
 	csrfOpts := CsrfOptions{
-		Secret:         setting.SecretKey,
+		Secret:         hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
 		Cookie:         setting.CSRFCookieName,
 		SetCookie:      true,
 		Secure:         setting.SessionConfig.Secure,
@@ -201,6 +192,7 @@ func Contexter() func(next http.Handler) http.Handler {
 			httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
 			ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
 
+			ctx.Data["SystemConfig"] = setting.Config()
 			ctx.Data["CsrfToken"] = ctx.Csrf.GetToken()
 			ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)
 
@@ -253,6 +245,13 @@ func (ctx *Context) JSONOK() {
 	ctx.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it
 }
 
-func (ctx *Context) JSONError(msg string) {
-	ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": msg})
+func (ctx *Context) JSONError(msg any) {
+	switch v := msg.(type) {
+	case string:
+		ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "text"})
+	case template.HTML:
+		ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "html"})
+	default:
+		panic(fmt.Sprintf("unsupported type: %T", msg))
+	}
 }
diff --git a/modules/context/context_cookie.go b/services/context/context_cookie.go
similarity index 100%
rename from modules/context/context_cookie.go
rename to services/context/context_cookie.go
diff --git a/modules/context/context_model.go b/services/context/context_model.go
similarity index 100%
rename from modules/context/context_model.go
rename to services/context/context_model.go
diff --git a/modules/context/context_request.go b/services/context/context_request.go
similarity index 100%
rename from modules/context/context_request.go
rename to services/context/context_request.go
diff --git a/modules/context/context_response.go b/services/context/context_response.go
similarity index 80%
rename from modules/context/context_response.go
rename to services/context/context_response.go
index 5729865561..d7fd18acac 100644
--- a/modules/context/context_response.go
+++ b/services/context/context_response.go
@@ -6,6 +6,7 @@ package context
 import (
 	"errors"
 	"fmt"
+	"html/template"
 	"net"
 	"net/http"
 	"net/url"
@@ -43,14 +44,14 @@ func RedirectToUser(ctx *Base, userName string, redirectUserID int64) {
 	ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
 }
 
-// RedirectToFirst redirects to first not empty URL
-func (ctx *Context) RedirectToFirst(location ...string) {
+// RedirectToCurrentSite redirects to first not empty URL which belongs to current site
+func (ctx *Context) RedirectToCurrentSite(location ...string) {
 	for _, loc := range location {
 		if len(loc) == 0 {
 			continue
 		}
 
-		if httplib.IsRiskyRedirectURL(loc) {
+		if !httplib.IsCurrentGiteaSiteURL(loc) {
 			continue
 		}
 
@@ -90,20 +91,33 @@ func (ctx *Context) HTML(status int, name base.TplName) {
 	}
 }
 
-// RenderToString renders the template content to a string
-func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) {
+// JSONTemplate renders the template as JSON response
+// keep in mind that the template is processed in HTML context, so JSON-things should be handled carefully, eg: by JSEscape
+func (ctx *Context) JSONTemplate(tmpl base.TplName) {
+	t, err := ctx.Render.TemplateLookup(string(tmpl), nil)
+	if err != nil {
+		ctx.ServerError("unable to find template", err)
+		return
+	}
+	ctx.Resp.Header().Set("Content-Type", "application/json")
+	if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
+		ctx.ServerError("unable to execute template", err)
+	}
+}
+
+// RenderToHTML renders the template content to a HTML string
+func (ctx *Context) RenderToHTML(name base.TplName, data map[string]any) (template.HTML, error) {
 	var buf strings.Builder
-	err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data, ctx.TemplateContext)
-	return buf.String(), err
+	err := ctx.Render.HTML(&buf, 0, string(name), data, ctx.TemplateContext)
+	return template.HTML(buf.String()), err
 }
 
 // RenderWithErr used for page has form validation but need to prompt error to users.
-func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form any) {
+func (ctx *Context) RenderWithErr(msg any, tpl base.TplName, form any) {
 	if form != nil {
 		middleware.AssignForm(form, ctx.Data)
 	}
-	ctx.Flash.ErrorMsg = msg
-	ctx.Data["Flash"] = ctx.Flash
+	ctx.Flash.Error(msg, true)
 	ctx.HTML(http.StatusOK, tpl)
 }
 
diff --git a/modules/context/context_template.go b/services/context/context_template.go
similarity index 53%
rename from modules/context/context_template.go
rename to services/context/context_template.go
index ba90fc170a..7878d409ca 100644
--- a/modules/context/context_template.go
+++ b/services/context/context_template.go
@@ -5,10 +5,7 @@ package context
 
 import (
 	"context"
-	"errors"
 	"time"
-
-	"code.gitea.io/gitea/modules/log"
 )
 
 var _ context.Context = TemplateContext(nil)
@@ -36,14 +33,3 @@ func (c TemplateContext) Err() error {
 func (c TemplateContext) Value(key any) any {
 	return c.parentContext().Value(key)
 }
-
-// DataRaceCheck checks whether the template context function "ctx()" returns the consistent context
-// as the current template's rendering context (request context), to help to find data race issues as early as possible.
-// When the code is proven to be correct and stable, this function should be removed.
-func (c TemplateContext) DataRaceCheck(dataCtx context.Context) (string, error) {
-	if c.parentContext() != dataCtx {
-		log.Error("TemplateContext.DataRaceCheck: parent context mismatch\n%s", log.Stack(2))
-		return "", errors.New("parent context mismatch")
-	}
-	return "", nil
-}
diff --git a/services/context/context_test.go b/services/context/context_test.go
new file mode 100644
index 0000000000..984593398d
--- /dev/null
+++ b/services/context/context_test.go
@@ -0,0 +1,51 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package context
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"testing"
+
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRemoveSessionCookieHeader(t *testing.T) {
+	w := httptest.NewRecorder()
+	w.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String())
+	w.Header().Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String())
+	assert.Len(t, w.Header().Values("Set-Cookie"), 2)
+	removeSessionCookieHeader(w)
+	assert.Len(t, w.Header().Values("Set-Cookie"), 1)
+	assert.Contains(t, "other=bar", w.Header().Get("Set-Cookie"))
+}
+
+func TestRedirectToCurrentSite(t *testing.T) {
+	defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
+	defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
+	cases := []struct {
+		location string
+		want     string
+	}{
+		{"/", "/sub/"},
+		{"http://localhost:3000/sub?k=v", "http://localhost:3000/sub?k=v"},
+		{"http://other", "/sub/"},
+	}
+	for _, c := range cases {
+		t.Run(c.location, func(t *testing.T) {
+			req := &http.Request{URL: &url.URL{Path: "/"}}
+			resp := httptest.NewRecorder()
+			base, baseCleanUp := NewBaseContext(resp, req)
+			defer baseCleanUp()
+			ctx := NewWebContext(base, nil, nil)
+			ctx.RedirectToCurrentSite(c.location)
+			redirect := test.RedirectURL(resp)
+			assert.Equal(t, c.want, redirect)
+		})
+	}
+}
diff --git a/modules/context/csrf.go b/services/context/csrf.go
similarity index 100%
rename from modules/context/csrf.go
rename to services/context/csrf.go
diff --git a/modules/context/org.go b/services/context/org.go
similarity index 93%
rename from modules/context/org.go
rename to services/context/org.go
index d068646577..018b76de43 100644
--- a/modules/context/org.go
+++ b/services/context/org.go
@@ -11,6 +11,8 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 )
@@ -255,6 +257,19 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
 	ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects)
 	ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages)
 	ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode)
+
+	ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
+	if len(ctx.ContextUser.Description) != 0 {
+		content, err := markdown.RenderString(&markup.RenderContext{
+			Metas: map[string]string{"mode": "document"},
+			Ctx:   ctx,
+		}, ctx.ContextUser.Description)
+		if err != nil {
+			ctx.ServerError("RenderString", err)
+			return
+		}
+		ctx.Data["RenderedDescription"] = content
+	}
 }
 
 // OrgAssignment returns a middleware to handle organization assignment
diff --git a/modules/context/package.go b/services/context/package.go
similarity index 100%
rename from modules/context/package.go
rename to services/context/package.go
diff --git a/modules/context/pagination.go b/services/context/pagination.go
similarity index 70%
rename from modules/context/pagination.go
rename to services/context/pagination.go
index 68237c630c..fb2ef699ce 100644
--- a/modules/context/pagination.go
+++ b/services/context/pagination.go
@@ -26,17 +26,6 @@ func NewPagination(total, pagingNum, current, numPages int) *Pagination {
 	return p
 }
 
-// AddParam adds a value from context identified by ctxKey as link param under a given paramKey
-func (p *Pagination) AddParam(ctx *Context, paramKey, ctxKey string) {
-	_, exists := ctx.Data[ctxKey]
-	if !exists {
-		return
-	}
-	paramData := fmt.Sprintf("%v", ctx.Data[ctxKey]) // cast any to string
-	urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(paramKey), url.QueryEscape(paramData))
-	p.urlParams = append(p.urlParams, urlParam)
-}
-
 // AddParamString adds a string parameter directly
 func (p *Pagination) AddParamString(key, value string) {
 	urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value))
@@ -50,8 +39,14 @@ func (p *Pagination) GetParams() template.URL {
 
 // SetDefaultParams sets common pagination params that are often used
 func (p *Pagination) SetDefaultParams(ctx *Context) {
-	p.AddParam(ctx, "sort", "SortType")
-	p.AddParam(ctx, "q", "Keyword")
+	if v, ok := ctx.Data["SortType"].(string); ok {
+		p.AddParamString("sort", v)
+	}
+	if v, ok := ctx.Data["Keyword"].(string); ok {
+		p.AddParamString("q", v)
+	}
+	if v, ok := ctx.Data["IsFuzzy"].(bool); ok {
+		p.AddParamString("fuzzy", fmt.Sprint(v))
+	}
 	// do not add any more uncommon params here!
-	p.AddParam(ctx, "t", "queryType")
 }
diff --git a/modules/context/permission.go b/services/context/permission.go
similarity index 100%
rename from modules/context/permission.go
rename to services/context/permission.go
diff --git a/modules/context/private.go b/services/context/private.go
similarity index 100%
rename from modules/context/private.go
rename to services/context/private.go
diff --git a/modules/context/repo.go b/services/context/repo.go
similarity index 96%
rename from modules/context/repo.go
rename to services/context/repo.go
index 75ebfec705..56e9fada0e 100644
--- a/modules/context/repo.go
+++ b/services/context/repo.go
@@ -6,6 +6,7 @@ package context
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"html"
 	"net/http"
@@ -26,6 +27,7 @@ import (
 	"code.gitea.io/gitea/modules/gitrepo"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
@@ -81,11 +83,15 @@ func (r *Repository) CanCreateBranch() bool {
 	return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
 }
 
+func (r *Repository) GetObjectFormat() git.ObjectFormat {
+	return git.ObjectFormatFromName(r.Repository.ObjectFormatName)
+}
+
 // RepoMustNotBeArchived checks if a repo is archived
 func RepoMustNotBeArchived() func(ctx *Context) {
 	return func(ctx *Context) {
 		if ctx.Repo.Repository.IsArchived {
-			ctx.NotFound("IsArchived", fmt.Errorf(ctx.Tr("repo.archive.title")))
+			ctx.NotFound("IsArchived", errors.New(ctx.Locale.TrString("repo.archive.title")))
 		}
 	}
 }
@@ -402,26 +408,6 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
 	ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
 }
 
-// RepoIDAssignment returns a handler which assigns the repo to the context.
-func RepoIDAssignment() func(ctx *Context) {
-	return func(ctx *Context) {
-		repoID := ctx.ParamsInt64(":repoid")
-
-		// Get repository.
-		repo, err := repo_model.GetRepositoryByID(ctx, repoID)
-		if err != nil {
-			if repo_model.IsErrRepoNotExist(err) {
-				ctx.NotFound("GetRepositoryByID", nil)
-			} else {
-				ctx.ServerError("GetRepositoryByID", err)
-			}
-			return
-		}
-
-		repoAssignment(ctx, repo)
-	}
-}
-
 // RepoAssignment returns a middleware to handle repository assignment
 func RepoAssignment(ctx *Context) context.CancelFunc {
 	if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce {
@@ -540,7 +526,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
 	ctx.Data["NumTags"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
 		IncludeDrafts: true,
 		IncludeTags:   true,
-		HasSha1:       util.OptionalBoolTrue, // only draft releases which are created with existing tags
+		HasSha1:       optional.Some(true), // only draft releases which are created with existing tags
 		RepoID:        ctx.Repo.Repository.ID,
 	})
 	if err != nil {
@@ -670,7 +656,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
 
 	branchOpts := git_model.FindBranchOptions{
 		RepoID:          ctx.Repo.Repository.ID,
-		IsDeletedBranch: util.OptionalBoolFalse,
+		IsDeletedBranch: optional.Some(false),
 		ListOptions:     db.ListOptionsAll,
 	}
 	branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts)
@@ -695,7 +681,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
 		if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) {
 			ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
 		} else {
-			ctx.Repo.BranchName, _ = gitRepo.GetDefaultBranch()
+			ctx.Repo.BranchName, _ = gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository)
 			if ctx.Repo.BranchName == "" {
 				// If it still can't get a default branch, fall back to default branch from setting.
 				// Something might be wrong. Either site admin should fix the repo sync or Gitea should fix a potential bug.
@@ -828,9 +814,8 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
 		}
 		// For legacy and API support only full commit sha
 		parts := strings.Split(path, "/")
-		objectFormat, _ := repo.GitRepo.GetObjectFormat()
 
-		if len(parts) > 0 && len(parts[0]) == objectFormat.FullLength() {
+		if len(parts) > 0 && len(parts[0]) == git.ObjectFormatFromName(repo.Repository.ObjectFormatName).FullLength() {
 			repo.TreePath = strings.Join(parts[1:], "/")
 			return parts[0]
 		}
@@ -874,9 +859,8 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
 		return getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsTagExist)
 	case RepoRefCommit:
 		parts := strings.Split(path, "/")
-		objectFormat, _ := repo.GitRepo.GetObjectFormat()
 
-		if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= objectFormat.FullLength() {
+		if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= repo.GetObjectFormat().FullLength() {
 			repo.TreePath = strings.Join(parts[1:], "/")
 			return parts[0]
 		}
@@ -935,12 +919,6 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
 			}
 		}
 
-		objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
-		if err != nil {
-			log.Error("Cannot determine objectFormat for repository: %w", err)
-			ctx.Repo.Repository.MarkAsBrokenEmpty()
-		}
-
 		// Get default branch.
 		if len(ctx.Params("*")) == 0 {
 			refName = ctx.Repo.Repository.DefaultBranch
@@ -1007,7 +985,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
 					return cancel
 				}
 				ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
-			} else if len(refName) >= 7 && len(refName) <= objectFormat.FullLength() {
+			} else if len(refName) >= 7 && len(refName) <= ctx.Repo.GetObjectFormat().FullLength() {
 				ctx.Repo.IsViewCommit = true
 				ctx.Repo.CommitID = refName
 
@@ -1017,7 +995,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
 					return cancel
 				}
 				// If short commit ID add canonical link header
-				if len(refName) < objectFormat.FullLength() {
+				if len(refName) < ctx.Repo.GetObjectFormat().FullLength() {
 					ctx.RespHeader().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"",
 						util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))))
 				}
diff --git a/modules/context/response.go b/services/context/response.go
similarity index 100%
rename from modules/context/response.go
rename to services/context/response.go
diff --git a/modules/upload/upload.go b/services/context/upload/upload.go
similarity index 98%
rename from modules/upload/upload.go
rename to services/context/upload/upload.go
index cd10715864..77a7eb9377 100644
--- a/modules/upload/upload.go
+++ b/services/context/upload/upload.go
@@ -11,9 +11,9 @@ import (
 	"regexp"
 	"strings"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
 )
 
 // ErrFileTypeForbidden not allowed file type error
diff --git a/modules/upload/upload_test.go b/services/context/upload/upload_test.go
similarity index 100%
rename from modules/upload/upload_test.go
rename to services/context/upload/upload_test.go
diff --git a/services/context/user.go b/services/context/user.go
index 8b2faf3369..4c9cd2928b 100644
--- a/services/context/user.go
+++ b/services/context/user.go
@@ -9,12 +9,11 @@ import (
 	"strings"
 
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 )
 
 // UserAssignmentWeb returns a middleware to handle context-user assignment for web routes
-func UserAssignmentWeb() func(ctx *context.Context) {
-	return func(ctx *context.Context) {
+func UserAssignmentWeb() func(ctx *Context) {
+	return func(ctx *Context) {
 		errorFn := func(status int, title string, obj any) {
 			err, ok := obj.(error)
 			if !ok {
@@ -32,8 +31,8 @@ func UserAssignmentWeb() func(ctx *context.Context) {
 }
 
 // UserIDAssignmentAPI returns a middleware to handle context-user assignment for api routes
-func UserIDAssignmentAPI() func(ctx *context.APIContext) {
-	return func(ctx *context.APIContext) {
+func UserIDAssignmentAPI() func(ctx *APIContext) {
+	return func(ctx *APIContext) {
 		userID := ctx.ParamsInt64(":user-id")
 
 		if ctx.IsSigned && ctx.Doer.ID == userID {
@@ -53,13 +52,13 @@ func UserIDAssignmentAPI() func(ctx *context.APIContext) {
 }
 
 // UserAssignmentAPI returns a middleware to handle context-user assignment for api routes
-func UserAssignmentAPI() func(ctx *context.APIContext) {
-	return func(ctx *context.APIContext) {
+func UserAssignmentAPI() func(ctx *APIContext) {
+	return func(ctx *APIContext) {
 		ctx.ContextUser = userAssignment(ctx.Base, ctx.Doer, ctx.Error)
 	}
 }
 
-func userAssignment(ctx *context.Base, doer *user_model.User, errCb func(int, string, any)) (contextUser *user_model.User) {
+func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string, any)) (contextUser *user_model.User) {
 	username := ctx.Params(":username")
 
 	if doer != nil && doer.LowerName == strings.ToLower(username) {
@@ -70,7 +69,7 @@ func userAssignment(ctx *context.Base, doer *user_model.User, errCb func(int, st
 		if err != nil {
 			if user_model.IsErrUserNotExist(err) {
 				if redirectUserID, err := user_model.LookupUserRedirect(ctx, username); err == nil {
-					context.RedirectToUser(ctx, username, redirectUserID)
+					RedirectToUser(ctx, username, redirectUserID)
 				} else if user_model.IsErrUserRedirectNotExist(err) {
 					errCb(http.StatusNotFound, "GetUserByName", err)
 				} else {
diff --git a/modules/context/utils.go b/services/context/utils.go
similarity index 100%
rename from modules/context/utils.go
rename to services/context/utils.go
diff --git a/modules/context/xsrf.go b/services/context/xsrf.go
similarity index 100%
rename from modules/context/xsrf.go
rename to services/context/xsrf.go
diff --git a/modules/context/xsrf_test.go b/services/context/xsrf_test.go
similarity index 100%
rename from modules/context/xsrf_test.go
rename to services/context/xsrf_test.go
diff --git a/modules/contexttest/context_tests.go b/services/contexttest/context_tests.go
similarity index 94%
rename from modules/contexttest/context_tests.go
rename to services/contexttest/context_tests.go
index c9bacf259f..3064c56590 100644
--- a/modules/contexttest/context_tests.go
+++ b/services/contexttest/context_tests.go
@@ -7,21 +7,23 @@ package contexttest
 import (
 	gocontext "context"
 	"io"
+	"maps"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
 	"strings"
 	"testing"
+	"time"
 
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/go-chi/chi/v5"
 	"github.com/stretchr/testify/assert"
@@ -35,7 +37,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request {
 	}
 	requestURL, err := url.Parse(path)
 	assert.NoError(t, err)
-	req := &http.Request{Method: method, URL: requestURL, Form: url.Values{}}
+	req := &http.Request{Method: method, URL: requestURL, Form: maps.Clone(requestURL.Query()), Header: http.Header{}}
 	req = req.WithContext(middleware.WithContextData(req.Context()))
 	return req
 }
@@ -61,7 +63,9 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
 	base.Locale = &translation.MockLocale{}
 
 	ctx := context.NewWebContext(base, opt.Render, nil)
-
+	ctx.AppendContextValue(context.WebContextKey, ctx)
+	ctx.PageData = map[string]any{}
+	ctx.Data["PageStartTime"] = time.Now()
 	chiCtx := chi.NewRouteContext()
 	ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
 	return ctx, resp
diff --git a/services/convert/git_commit.go b/services/convert/git_commit.go
index ed08691c8b..e0efcddbcb 100644
--- a/services/convert/git_commit.go
+++ b/services/convert/git_commit.go
@@ -10,11 +10,11 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	ctx "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	ctx "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/gitdiff"
 )
 
diff --git a/services/convert/issue.go b/services/convert/issue.go
index c6e06180c8..54b00cd88e 100644
--- a/services/convert/issue.go
+++ b/services/convert/issue.go
@@ -18,19 +18,19 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 )
 
-func ToIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
-	return toIssue(ctx, issue, WebAssetDownloadURL)
+func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue {
+	return toIssue(ctx, doer, issue, WebAssetDownloadURL)
 }
 
 // ToAPIIssue converts an Issue to API format
 // it assumes some fields assigned with values:
 // Required - Poster, Labels,
 // Optional - Milestone, Assignee, PullRequest
-func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
-	return toIssue(ctx, issue, APIAssetDownloadURL)
+func ToAPIIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue {
+	return toIssue(ctx, doer, issue, APIAssetDownloadURL)
 }
 
-func toIssue(ctx context.Context, issue *issues_model.Issue, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Issue {
+func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Issue {
 	if err := issue.LoadLabels(ctx); err != nil {
 		return &api.Issue{}
 	}
@@ -44,7 +44,7 @@ func toIssue(ctx context.Context, issue *issues_model.Issue, getDownloadURL func
 	apiIssue := &api.Issue{
 		ID:          issue.ID,
 		Index:       issue.Index,
-		Poster:      ToUser(ctx, issue.Poster, nil),
+		Poster:      ToUser(ctx, issue.Poster, doer),
 		Title:       issue.Title,
 		Body:        issue.Content,
 		Attachments: toAttachments(issue.Repo, issue.Attachments, getDownloadURL),
@@ -114,25 +114,25 @@ func toIssue(ctx context.Context, issue *issues_model.Issue, getDownloadURL func
 }
 
 // ToIssueList converts an IssueList to API format
-func ToIssueList(ctx context.Context, il issues_model.IssueList) []*api.Issue {
+func ToIssueList(ctx context.Context, doer *user_model.User, il issues_model.IssueList) []*api.Issue {
 	result := make([]*api.Issue, len(il))
 	for i := range il {
-		result[i] = ToIssue(ctx, il[i])
+		result[i] = ToIssue(ctx, doer, il[i])
 	}
 	return result
 }
 
 // ToAPIIssueList converts an IssueList to API format
-func ToAPIIssueList(ctx context.Context, il issues_model.IssueList) []*api.Issue {
+func ToAPIIssueList(ctx context.Context, doer *user_model.User, il issues_model.IssueList) []*api.Issue {
 	result := make([]*api.Issue, len(il))
 	for i := range il {
-		result[i] = ToAPIIssue(ctx, il[i])
+		result[i] = ToAPIIssue(ctx, doer, il[i])
 	}
 	return result
 }
 
 // ToTrackedTime converts TrackedTime to API format
-func ToTrackedTime(ctx context.Context, t *issues_model.TrackedTime) (apiT *api.TrackedTime) {
+func ToTrackedTime(ctx context.Context, doer *user_model.User, t *issues_model.TrackedTime) (apiT *api.TrackedTime) {
 	apiT = &api.TrackedTime{
 		ID:      t.ID,
 		IssueID: t.IssueID,
@@ -141,7 +141,7 @@ func ToTrackedTime(ctx context.Context, t *issues_model.TrackedTime) (apiT *api.
 		Created: t.Created,
 	}
 	if t.Issue != nil {
-		apiT.Issue = ToAPIIssue(ctx, t.Issue)
+		apiT.Issue = ToAPIIssue(ctx, doer, t.Issue)
 	}
 	if t.User != nil {
 		apiT.UserName = t.User.Name
@@ -192,10 +192,10 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop
 }
 
 // ToTrackedTimeList converts TrackedTimeList to API format
-func ToTrackedTimeList(ctx context.Context, tl issues_model.TrackedTimeList) api.TrackedTimeList {
+func ToTrackedTimeList(ctx context.Context, doer *user_model.User, tl issues_model.TrackedTimeList) api.TrackedTimeList {
 	result := make([]*api.TrackedTime, 0, len(tl))
 	for _, t := range tl {
-		result = append(result, ToTrackedTime(ctx, t))
+		result = append(result, ToTrackedTime(ctx, doer, t))
 	}
 	return result
 }
diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go
index b034a50897..9ffaf1e84c 100644
--- a/services/convert/issue_comment.go
+++ b/services/convert/issue_comment.go
@@ -120,7 +120,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
 			return nil
 		}
 
-		comment.TrackedTime = ToTrackedTime(ctx, c.Time)
+		comment.TrackedTime = ToTrackedTime(ctx, doer, c.Time)
 	}
 
 	if c.RefIssueID != 0 {
@@ -129,7 +129,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
 			log.Error("GetIssueByID(%d): %v", c.RefIssueID, err)
 			return nil
 		}
-		comment.RefIssue = ToAPIIssue(ctx, issue)
+		comment.RefIssue = ToAPIIssue(ctx, doer, issue)
 	}
 
 	if c.RefCommentID != 0 {
@@ -180,7 +180,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
 	}
 
 	if c.DependentIssue != nil {
-		comment.DependentIssue = ToAPIIssue(ctx, c.DependentIssue)
+		comment.DependentIssue = ToAPIIssue(ctx, doer, c.DependentIssue)
 	}
 
 	return comment
diff --git a/services/convert/notification.go b/services/convert/notification.go
index 0b97530d8b..41063cf399 100644
--- a/services/convert/notification.go
+++ b/services/convert/notification.go
@@ -61,8 +61,9 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification)
 				result.Subject.LatestCommentHTMLURL = comment.HTMLURL(ctx)
 			}
 
-			pr, _ := n.Issue.GetPullRequest(ctx)
-			if pr != nil && pr.HasMerged {
+			if err := n.Issue.LoadPullRequest(ctx); err == nil &&
+				n.Issue.PullRequest != nil &&
+				n.Issue.PullRequest.HasMerged {
 				result.Subject.State = "merged"
 			}
 		}
diff --git a/services/convert/package.go b/services/convert/package.go
index e90ce8a00f..b5fca21a3c 100644
--- a/services/convert/package.go
+++ b/services/convert/package.go
@@ -35,7 +35,7 @@ func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_m
 		Name:       pd.Package.Name,
 		Version:    pd.Version.Version,
 		CreatedAt:  pd.Version.CreatedUnix.AsTime(),
-		HTMLURL:    pd.FullWebLink(),
+		HTMLURL:    pd.VersionHTMLURL(),
 	}, nil
 }
 
diff --git a/services/convert/pull.go b/services/convert/pull.go
index 6d98121ed5..775bf3806d 100644
--- a/services/convert/pull.go
+++ b/services/convert/pull.go
@@ -33,7 +33,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
 		return nil
 	}
 
-	apiIssue := ToAPIIssue(ctx, pr.Issue)
+	apiIssue := ToAPIIssue(ctx, doer, pr.Issue)
 	if err := pr.LoadBaseRepo(ctx); err != nil {
 		log.Error("GetRepositoryById[%d]: %v", pr.ID, err)
 		return nil
diff --git a/services/convert/pull_review.go b/services/convert/pull_review.go
index aa7ad68a47..29a5ab7466 100644
--- a/services/convert/pull_review.go
+++ b/services/convert/pull_review.go
@@ -66,7 +66,7 @@ func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user
 	result := make([]*api.PullReview, 0, len(rl))
 	for i := range rl {
 		// show pending reviews only for the user who created them
-		if rl[i].Type == issues_model.ReviewTypePending && !(doer.IsAdmin || doer.ID == rl[i].ReviewerID) {
+		if rl[i].Type == issues_model.ReviewTypePending && (doer == nil || (!doer.IsAdmin && doer.ID != rl[i].ReviewerID)) {
 			continue
 		}
 		r, err := ToPullReview(ctx, rl[i], doer)
diff --git a/services/convert/pull_review_test.go b/services/convert/pull_review_test.go
new file mode 100644
index 0000000000..6886950280
--- /dev/null
+++ b/services/convert/pull_review_test.go
@@ -0,0 +1,52 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_ToPullReview(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 6})
+	assert.EqualValues(t, reviewer.ID, review.ReviewerID)
+	assert.EqualValues(t, issues_model.ReviewTypePending, review.Type)
+
+	reviewList := []*issues_model.Review{review}
+
+	t.Run("Anonymous User", func(t *testing.T) {
+		prList, err := ToPullReviewList(db.DefaultContext, reviewList, nil)
+		assert.NoError(t, err)
+		assert.Empty(t, prList)
+	})
+
+	t.Run("Reviewer Himself", func(t *testing.T) {
+		prList, err := ToPullReviewList(db.DefaultContext, reviewList, reviewer)
+		assert.NoError(t, err)
+		assert.Len(t, prList, 1)
+	})
+
+	t.Run("Other User", func(t *testing.T) {
+		user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+		prList, err := ToPullReviewList(db.DefaultContext, reviewList, user4)
+		assert.NoError(t, err)
+		assert.Len(t, prList, 0)
+	})
+
+	t.Run("Admin User", func(t *testing.T) {
+		adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+		prList, err := ToPullReviewList(db.DefaultContext, reviewList, adminUser)
+		assert.NoError(t, err)
+		assert.Len(t, prList, 1)
+	})
+}
diff --git a/services/convert/repository.go b/services/convert/repository.go
index c16180c0af..39efd304a9 100644
--- a/services/convert/repository.go
+++ b/services/convert/repository.go
@@ -93,6 +93,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 	allowRebase := false
 	allowRebaseMerge := false
 	allowSquash := false
+	allowFastForwardOnly := false
 	allowRebaseUpdate := false
 	defaultDeleteBranchAfterMerge := false
 	defaultMergeStyle := repo_model.MergeStyleMerge
@@ -105,14 +106,18 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 		allowRebase = config.AllowRebase
 		allowRebaseMerge = config.AllowRebaseMerge
 		allowSquash = config.AllowSquash
+		allowFastForwardOnly = config.AllowFastForwardOnly
 		allowRebaseUpdate = config.AllowRebaseUpdate
 		defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
 		defaultMergeStyle = config.GetDefaultMergeStyle()
 		defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
 	}
 	hasProjects := false
-	if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
+	projectsMode := repo_model.ProjectsModeAll
+	if unit, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
 		hasProjects = true
+		config := unit.ProjectsConfig()
+		projectsMode = config.ProjectsMode
 	}
 
 	hasReleases := false
@@ -209,6 +214,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 		InternalTracker:               internalTracker,
 		HasWiki:                       hasWiki,
 		HasProjects:                   hasProjects,
+		ProjectsMode:                  string(projectsMode),
 		HasReleases:                   hasReleases,
 		HasPackages:                   hasPackages,
 		HasActions:                    hasActions,
@@ -219,6 +225,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 		AllowRebase:                   allowRebase,
 		AllowRebaseMerge:              allowRebaseMerge,
 		AllowSquash:                   allowSquash,
+		AllowFastForwardOnly:          allowFastForwardOnly,
 		AllowRebaseUpdate:             allowRebaseUpdate,
 		DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
 		DefaultMergeStyle:             string(defaultMergeStyle),
diff --git a/services/cron/setting.go b/services/cron/setting.go
index 0656307cba..6dad88830a 100644
--- a/services/cron/setting.go
+++ b/services/cron/setting.go
@@ -70,7 +70,7 @@ func (b *BaseConfig) DoNoticeOnSuccess() bool {
 // Please note the `status` string will be concatenated with `admin.dashboard.cron.` and `admin.dashboard.task.` to provide locale messages. Similarly `name` will be composed with `admin.dashboard.` to provide the locale name for the task.
 func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer string, args ...any) string {
 	realArgs := make([]any, 0, len(args)+2)
-	realArgs = append(realArgs, locale.Tr("admin.dashboard."+name))
+	realArgs = append(realArgs, locale.TrString("admin.dashboard."+name))
 	if doer == "" {
 		realArgs = append(realArgs, "(Cron)")
 	} else {
@@ -80,7 +80,7 @@ func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer
 		realArgs = append(realArgs, args...)
 	}
 	if doer == "" {
-		return locale.Tr("admin.dashboard.cron."+status, realArgs...)
+		return locale.TrString("admin.dashboard.cron."+status, realArgs...)
 	}
-	return locale.Tr("admin.dashboard.task."+status, realArgs...)
+	return locale.TrString("admin.dashboard.task."+status, realArgs...)
 }
diff --git a/services/cron/tasks.go b/services/cron/tasks.go
index f0956a97d8..f8a7444c49 100644
--- a/services/cron/tasks.go
+++ b/services/cron/tasks.go
@@ -159,7 +159,7 @@ func RegisterTask(name string, config Config, fun func(context.Context, *user_mo
 	log.Debug("Registering task: %s", name)
 
 	i18nKey := "admin.dashboard." + name
-	if value := translation.NewLocale("en-US").Tr(i18nKey); value == i18nKey {
+	if value := translation.NewLocale("en-US").TrString(i18nKey); value == i18nKey {
 		return fmt.Errorf("translation is missing for task %q, please add translation for %q", name, i18nKey)
 	}
 
diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go
index 1dd5d70a38..0018c5facc 100644
--- a/services/cron/tasks_extended.go
+++ b/services/cron/tasks_extended.go
@@ -8,13 +8,13 @@ import (
 	"time"
 
 	activities_model "code.gitea.io/gitea/models/activities"
-	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/system"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/updatechecker"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	repo_service "code.gitea.io/gitea/services/repository"
 	archiver_service "code.gitea.io/gitea/services/repository/archiver"
 	user_service "code.gitea.io/gitea/services/user"
@@ -71,7 +71,7 @@ func registerRewriteAllPublicKeys() {
 		RunAtStart: false,
 		Schedule:   "@every 72h",
 	}, func(ctx context.Context, _ *user_model.User, _ Config) error {
-		return asymkey_model.RewriteAllPublicKeys(ctx)
+		return asymkey_service.RewriteAllPublicKeys(ctx)
 	})
 }
 
@@ -81,7 +81,7 @@ func registerRewriteAllPrincipalKeys() {
 		RunAtStart: false,
 		Schedule:   "@every 72h",
 	}, func(ctx context.Context, _ *user_model.User, _ Config) error {
-		return asymkey_model.RewriteAllPrincipalKeys(ctx)
+		return asymkey_service.RewriteAllPrincipalKeys(ctx)
 	})
 }
 
diff --git a/services/doctor/authorizedkeys.go b/services/doctor/authorizedkeys.go
index 050a4e7974..8d6fc9cb5e 100644
--- a/services/doctor/authorizedkeys.go
+++ b/services/doctor/authorizedkeys.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 )
 
 const tplCommentPrefix = `# gitea public key`
@@ -33,7 +34,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
 			return fmt.Errorf("Unable to open authorized_keys file. ERROR: %w", err)
 		}
 		logger.Warn("Unable to open authorized_keys. (ERROR: %v). Attempting to rewrite...", err)
-		if err = asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+		if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
 			logger.Critical("Unable to rewrite authorized_keys file. ERROR: %v", err)
 			return fmt.Errorf("Unable to rewrite authorized_keys file. ERROR: %w", err)
 		}
@@ -50,7 +51,11 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
 		}
 		linesInAuthorizedKeys.Add(line)
 	}
-	f.Close()
+	if err = scanner.Err(); err != nil {
+		return fmt.Errorf("scan: %w", err)
+	}
+	// although there is a "defer close" above, here close explicitly before the generating, because it needs to open the file for writing again
+	_ = f.Close()
 
 	// now we regenerate and check if there are any lines missing
 	regenerated := &bytes.Buffer{}
@@ -76,7 +81,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
 			return fmt.Errorf(`authorized_keys is out of date and should be regenerated with "gitea admin regenerate keys" or "gitea doctor --run authorized-keys --fix"`)
 		}
 		logger.Warn("authorized_keys is out of date. Attempting rewrite...")
-		err = asymkey_model.RewriteAllPublicKeys(ctx)
+		err = asymkey_service.RewriteAllPublicKeys(ctx)
 		if err != nil {
 			logger.Critical("Unable to rewrite authorized_keys file. ERROR: %v", err)
 			return fmt.Errorf("Unable to rewrite authorized_keys file. ERROR: %w", err)
diff --git a/services/doctor/dbconsistency.go b/services/doctor/dbconsistency.go
index e2dcb63f33..dfdf7b547a 100644
--- a/services/doctor/dbconsistency.go
+++ b/services/doctor/dbconsistency.go
@@ -61,26 +61,20 @@ func asFixer(fn func(ctx context.Context) error) func(ctx context.Context) (int6
 	}
 }
 
-func genericOrphanCheck(name, subject, refobject, joincond string) consistencyCheck {
+func genericOrphanCheck(name, subject, refObject, joinCond string) consistencyCheck {
 	return consistencyCheck{
 		Name: name,
 		Counter: func(ctx context.Context) (int64, error) {
-			return db.CountOrphanedObjects(ctx, subject, refobject, joincond)
+			return db.CountOrphanedObjects(ctx, subject, refObject, joinCond)
 		},
 		Fixer: func(ctx context.Context) (int64, error) {
-			err := db.DeleteOrphanedObjects(ctx, subject, refobject, joincond)
+			err := db.DeleteOrphanedObjects(ctx, subject, refObject, joinCond)
 			return -1, err
 		},
 	}
 }
 
-func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) error {
-	// make sure DB version is uptodate
-	if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
-		logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
-		return err
-	}
-
+func prepareDBConsistencyChecks() []consistencyCheck {
 	consistencyChecks := []consistencyCheck{
 		{
 			// find labels without existing repo or org
@@ -210,7 +204,7 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
 			"oauth2_grant", "user", "oauth2_grant.user_id=`user`.id"),
 		// find OAuth2Application without existing user
 		genericOrphanCheck("Orphaned OAuth2Application without existing User",
-			"oauth2_application", "user", "oauth2_application.uid=`user`.id"),
+			"oauth2_application", "user", "oauth2_application.uid=0 OR oauth2_application.uid=`user`.id"),
 		// find OAuth2AuthorizationCode without existing OAuth2Grant
 		genericOrphanCheck("Orphaned OAuth2AuthorizationCode without existing OAuth2Grant",
 			"oauth2_authorization_code", "oauth2_grant", "oauth2_authorization_code.grant_id=oauth2_grant.id"),
@@ -224,7 +218,16 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
 		genericOrphanCheck("Orphaned Redirects without existing redirect user",
 			"user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"),
 	)
+	return consistencyChecks
+}
 
+func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) error {
+	// make sure DB version is uptodate
+	if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
+		logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
+		return err
+	}
+	consistencyChecks := prepareDBConsistencyChecks()
 	for _, c := range consistencyChecks {
 		if err := c.Run(ctx, logger, autofix); err != nil {
 			return err
diff --git a/services/doctor/dbconsistency_test.go b/services/doctor/dbconsistency_test.go
new file mode 100644
index 0000000000..4e4ac535b7
--- /dev/null
+++ b/services/doctor/dbconsistency_test.go
@@ -0,0 +1,51 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package doctor
+
+import (
+	"slices"
+	"testing"
+
+	"code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/log"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConsistencyCheck(t *testing.T) {
+	checks := prepareDBConsistencyChecks()
+	idx := slices.IndexFunc(checks, func(check consistencyCheck) bool {
+		return check.Name == "Orphaned OAuth2Application without existing User"
+	})
+	if !assert.NotEqual(t, -1, idx) {
+		return
+	}
+
+	_ = db.TruncateBeans(db.DefaultContext, &auth.OAuth2Application{}, &user.User{})
+	_ = db.TruncateBeans(db.DefaultContext, &auth.OAuth2Application{}, &auth.OAuth2Application{})
+
+	err := db.Insert(db.DefaultContext, &user.User{ID: 1})
+	assert.NoError(t, err)
+	err = db.Insert(db.DefaultContext, &auth.OAuth2Application{Name: "test-oauth2-app-1", ClientID: "client-id-1"})
+	assert.NoError(t, err)
+	err = db.Insert(db.DefaultContext, &auth.OAuth2Application{Name: "test-oauth2-app-2", ClientID: "client-id-2", UID: 1})
+	assert.NoError(t, err)
+	err = db.Insert(db.DefaultContext, &auth.OAuth2Application{Name: "test-oauth2-app-3", ClientID: "client-id-3", UID: 99999999})
+	assert.NoError(t, err)
+
+	unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-1"})
+	unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-2"})
+	unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-3"})
+
+	oauth2AppCheck := checks[idx]
+	err = oauth2AppCheck.Run(db.DefaultContext, log.GetManager().GetLogger(log.DEFAULT), true)
+	assert.NoError(t, err)
+
+	unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-1"})
+	unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-2"})
+	unittest.AssertNotExistsBean(t, &auth.OAuth2Application{ClientID: "client-id-3"})
+}
diff --git a/services/doctor/doctor.go b/services/doctor/doctor.go
index 559f8e06da..a4eb5e16b9 100644
--- a/services/doctor/doctor.go
+++ b/services/doctor/doctor.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/storage"
 )
 
 // Check represents a Doctor check
@@ -25,6 +26,7 @@ type Check struct {
 	AbortIfFailed              bool
 	SkipDatabaseInitialization bool
 	Priority                   int
+	InitStorage                bool
 }
 
 func initDBSkipLogger(ctx context.Context) error {
@@ -84,6 +86,7 @@ func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) err
 	logger := log.BaseLoggerToGeneralLogger(&doctorCheckLogger{colorize: colorize})
 	loggerStep := log.BaseLoggerToGeneralLogger(&doctorCheckStepLogger{colorize: colorize})
 	dbIsInit := false
+	storageIsInit := false
 	for i, check := range checks {
 		if !dbIsInit && !check.SkipDatabaseInitialization {
 			// Only open database after the most basic configuration check
@@ -94,6 +97,14 @@ func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) err
 			}
 			dbIsInit = true
 		}
+		if !storageIsInit && check.InitStorage {
+			if err := storage.Init(); err != nil {
+				logger.Error("Error whilst initializing the storage: %v", err)
+				logger.Error("Check if you are using the right config file. You can use a --config directive to specify one.")
+				return nil
+			}
+			storageIsInit = true
+		}
 		logger.Info("\n[%d] %s", i+1, check.Title)
 		if err := check.Run(ctx, loggerStep, autofix); err != nil {
 			if check.AbortIfFailed {
diff --git a/services/doctor/fix16961.go b/services/doctor/fix16961.go
index d3f36d8d5c..50d9ac6621 100644
--- a/services/doctor/fix16961.go
+++ b/services/doctor/fix16961.go
@@ -216,6 +216,12 @@ func fixBrokenRepoUnit16961(repoUnit *repo_model.RepoUnit, bs []byte) (fixed boo
 		return false, nil
 	}
 
+	var cfg any
+	err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
+	if err == nil {
+		return false, nil
+	}
+
 	switch repoUnit.Type {
 	case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects:
 		cfg := &repo_model.UnitConfig{}
diff --git a/services/doctor/main_test.go b/services/doctor/main_test.go
new file mode 100644
index 0000000000..0f365e21d0
--- /dev/null
+++ b/services/doctor/main_test.go
@@ -0,0 +1,14 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package doctor
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+	unittest.MainTest(m)
+}
diff --git a/services/doctor/storage.go b/services/doctor/storage.go
index f338537864..787df27549 100644
--- a/services/doctor/storage.go
+++ b/services/doctor/storage.go
@@ -162,7 +162,7 @@ func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger lo
 		if opts.RepoArchives || opts.All {
 			if err := commonCheckStorage(ctx, logger, autofix,
 				&commonStorageCheckOptions{
-					storer: storage.RepoAvatars,
+					storer: storage.RepoArchives,
 					isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
 						exists, err := repo.ExistsRepoArchiverWithStoragePath(ctx, path)
 						if err == nil || errors.Is(err, util.ErrInvalidArgument) {
diff --git a/services/forms/admin.go b/services/forms/admin.go
index 4b3cacc606..81276f8f46 100644
--- a/services/forms/admin.go
+++ b/services/forms/admin.go
@@ -6,9 +6,9 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
@@ -41,6 +41,7 @@ type AdminEditUserForm struct {
 	Password                string `binding:"MaxSize(255)"`
 	Website                 string `binding:"ValidUrl;MaxSize(255)"`
 	Location                string `binding:"MaxSize(50)"`
+	Language                string `binding:"MaxSize(5)"`
 	MaxRepoCreation         int
 	Active                  bool
 	Admin                   bool
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index 25acbbb99e..c9f3182b3a 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -6,8 +6,8 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/org.go b/services/forms/org.go
index 6e2d787516..3677fcf429 100644
--- a/services/forms/org.go
+++ b/services/forms/org.go
@@ -7,9 +7,9 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/package_form.go b/services/forms/package_form.go
index 2f08dfe9f4..cc940d42d3 100644
--- a/services/forms/package_form.go
+++ b/services/forms/package_form.go
@@ -6,8 +6,8 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/repo_branch_form.go b/services/forms/repo_branch_form.go
index 5deb0ae463..42e6c85c37 100644
--- a/services/forms/repo_branch_form.go
+++ b/services/forms/repo_branch_form.go
@@ -6,8 +6,8 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 845eccf817..e45a2a1695 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -12,10 +12,10 @@ import (
 	"code.gitea.io/gitea/models"
 	issues_model "code.gitea.io/gitea/models/issues"
 	project_model "code.gitea.io/gitea/models/project"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/webhook"
 
 	"gitea.com/go-chi/binding"
@@ -133,6 +133,7 @@ type RepoSettingForm struct {
 	EnableCode                            bool
 	EnableWiki                            bool
 	EnableExternalWiki                    bool
+	DefaultWikiBranch                     string
 	ExternalWikiURL                       string
 	EnableIssues                          bool
 	EnableExternalTracker                 bool
@@ -142,6 +143,7 @@ type RepoSettingForm struct {
 	ExternalTrackerRegexpPattern          string
 	EnableCloseIssuesViaCommitInAnyBranch bool
 	EnableProjects                        bool
+	ProjectsMode                          string
 	EnableReleases                        bool
 	EnablePackages                        bool
 	EnablePulls                           bool
@@ -151,6 +153,7 @@ type RepoSettingForm struct {
 	PullsAllowRebase                      bool
 	PullsAllowRebaseMerge                 bool
 	PullsAllowSquash                      bool
+	PullsAllowFastForwardOnly             bool
 	PullsAllowManualMerge                 bool
 	PullsDefaultMergeStyle                string
 	EnableAutodetectManualMerge           bool
@@ -313,7 +316,7 @@ func (f *NewSlackHookForm) Validate(req *http.Request, errs binding.Errors) bind
 		errs = append(errs, binding.Error{
 			FieldNames:     []string{"Channel"},
 			Classification: "",
-			Message:        ctx.Tr("repo.settings.add_webhook.invalid_channel_name"),
+			Message:        ctx.Locale.TrString("repo.settings.add_webhook.invalid_channel_name"),
 		})
 	}
 	return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
@@ -598,8 +601,8 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors)
 // swagger:model MergePullRequestOption
 type MergePullRequestForm struct {
 	// required: true
-	// enum: merge,rebase,rebase-merge,squash,manually-merged
-	Do                     string `binding:"Required;In(merge,rebase,rebase-merge,squash,manually-merged)"`
+	// enum: merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged
+	Do                     string `binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"`
 	MergeTitleField        string
 	MergeMessageField      string
 	MergeCommitID          string // only used for manually-merged
@@ -625,6 +628,7 @@ type CodeCommentForm struct {
 	SingleReview   bool   `form:"single_review"`
 	Reply          int64  `form:"reply"`
 	LatestCommitID string
+	Files          []string
 }
 
 // Validate validates the fields
diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go
index 4dd99f9e32..0135684737 100644
--- a/services/forms/repo_tag_form.go
+++ b/services/forms/repo_tag_form.go
@@ -6,8 +6,8 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/runner.go b/services/forms/runner.go
index 6d16cfce49..6abfc66fc2 100644
--- a/services/forms/runner.go
+++ b/services/forms/runner.go
@@ -6,8 +6,8 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index cbab274238..e2e6c208f7 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -10,11 +10,11 @@ import (
 	"strings"
 
 	auth_model "code.gitea.io/gitea/models/auth"
-	"code.gitea.io/gitea/modules/context"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
@@ -109,11 +109,7 @@ func (f *RegisterForm) Validate(req *http.Request, errs binding.Errors) binding.
 // domains in the whitelist or if it doesn't match any of
 // domains in the blocklist, if any such list is not empty.
 func (f *RegisterForm) IsEmailDomainAllowed() bool {
-	if len(setting.Service.EmailDomainAllowList) == 0 {
-		return !validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, f.Email)
-	}
-
-	return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, f.Email)
+	return user_model.IsEmailDomainAllowed(f.Email)
 }
 
 // MustChangePasswordForm form for updating your password after account creation
@@ -449,3 +445,14 @@ func (f *PackageSettingForm) Validate(req *http.Request, errs binding.Errors) bi
 	ctx := context.GetValidateContext(req)
 	return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
 }
+
+type BlockUserForm struct {
+	Action  string `binding:"Required;In(block,unblock,note)"`
+	Blockee string `binding:"Required"`
+	Note    string
+}
+
+func (f *BlockUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
+	ctx := context.GetValidateContext(req)
+	return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
+}
diff --git a/services/forms/user_form_auth_openid.go b/services/forms/user_form_auth_openid.go
index d8137a8d13..ca1c77e320 100644
--- a/services/forms/user_form_auth_openid.go
+++ b/services/forms/user_form_auth_openid.go
@@ -6,8 +6,8 @@ package forms
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/context"
 
 	"gitea.com/go-chi/binding"
 )
diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go
index 03e629a553..c21fddf478 100644
--- a/services/forms/user_form_hidden_comments.go
+++ b/services/forms/user_form_hidden_comments.go
@@ -7,8 +7,8 @@ import (
 	"math/big"
 
 	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
 )
 
 type hiddenCommentTypeGroupsType map[string][]issues_model.CommentType
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index 0f6e2b6c17..b05c210a0c 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -29,6 +29,7 @@ import (
 	"code.gitea.io/gitea/modules/highlight"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/translation"
 
@@ -153,7 +154,7 @@ func (d *DiffLine) GetBlobExcerptQuery() string {
 
 // GetExpandDirection gets DiffLineExpandDirection
 func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection {
-	if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 {
+	if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.LeftIdx-d.SectionInfo.LastLeftIdx <= 1 || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 {
 		return DiffLineExpandNone
 	}
 	if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 {
@@ -1181,41 +1182,30 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
 
 	for _, diffFile := range diff.Files {
 
-		gotVendor := false
-		gotGenerated := false
+		isVendored := optional.None[bool]()
+		isGenerated := optional.None[bool]()
 		if checker != nil {
 			attrs, err := checker.CheckPath(diffFile.Name)
 			if err == nil {
-				if vendored, has := attrs["linguist-vendored"]; has {
-					if vendored == "set" || vendored == "true" {
-						diffFile.IsVendored = true
-						gotVendor = true
-					} else {
-						gotVendor = vendored == "false"
-					}
-				}
-				if generated, has := attrs["linguist-generated"]; has {
-					if generated == "set" || generated == "true" {
-						diffFile.IsGenerated = true
-						gotGenerated = true
-					} else {
-						gotGenerated = generated == "false"
-					}
-				}
-				if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
-					diffFile.Language = language
-				} else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
-					diffFile.Language = language
+				isVendored = git.AttributeToBool(attrs, git.AttributeLinguistVendored)
+				isGenerated = git.AttributeToBool(attrs, git.AttributeLinguistGenerated)
+
+				language := git.TryReadLanguageAttribute(attrs)
+				if language.Has() {
+					diffFile.Language = language.Value()
 				}
 			}
 		}
 
-		if !gotVendor {
-			diffFile.IsVendored = analyze.IsVendor(diffFile.Name)
+		if !isVendored.Has() {
+			isVendored = optional.Some(analyze.IsVendor(diffFile.Name))
 		}
-		if !gotGenerated {
-			diffFile.IsGenerated = analyze.IsGenerated(diffFile.Name)
+		diffFile.IsVendored = isVendored.Value()
+
+		if !isGenerated.Has() {
+			isGenerated = optional.Some(analyze.IsGenerated(diffFile.Name))
 		}
+		diffFile.IsGenerated = isGenerated.Value()
 
 		tailSection := diffFile.GetTailSection(gitRepo, opts.BeforeCommitID, opts.AfterCommitID)
 		if tailSection != nil {
diff --git a/services/issue/assignee.go b/services/issue/assignee.go
index 27fc695533..8740a6664a 100644
--- a/services/issue/assignee.go
+++ b/services/issue/assignee.go
@@ -10,6 +10,7 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
@@ -113,10 +114,10 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
 		return err
 	}
 
-	var pemResult bool
+	canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue)
+
 	if isAdd {
-		pemResult = permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests)
-		if !pemResult {
+		if !permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) {
 			return issues_model.ErrNotValidReviewRequest{
 				Reason: "Reviewer can't read",
 				UserID: doer.ID,
@@ -124,28 +125,6 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
 			}
 		}
 
-		if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest {
-			return nil
-		}
-
-		pemResult = doer.ID == issue.PosterID
-		if !pemResult {
-			pemResult = permDoer.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests)
-		}
-		if !pemResult {
-			pemResult, err = issues_model.IsOfficialReviewer(ctx, issue, doer)
-			if err != nil {
-				return err
-			}
-			if !pemResult {
-				return issues_model.ErrNotValidReviewRequest{
-					Reason: "Doer can't choose reviewer",
-					UserID: doer.ID,
-					RepoID: issue.Repo.ID,
-				}
-			}
-		}
-
 		if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 {
 			return issues_model.ErrNotValidReviewRequest{
 				Reason: "poster of pr can't be reviewer",
@@ -153,22 +132,35 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
 				RepoID: issue.Repo.ID,
 			}
 		}
-	} else {
-		if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID {
+
+		if canDoerChangeReviewRequests {
 			return nil
 		}
 
-		pemResult = permDoer.IsAdmin()
-		if !pemResult {
-			return issues_model.ErrNotValidReviewRequest{
-				Reason: "Doer is not admin",
-				UserID: doer.ID,
-				RepoID: issue.Repo.ID,
-			}
+		if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest {
+			return nil
+		}
+
+		return issues_model.ErrNotValidReviewRequest{
+			Reason: "Doer can't choose reviewer",
+			UserID: doer.ID,
+			RepoID: issue.Repo.ID,
 		}
 	}
 
-	return nil
+	if canDoerChangeReviewRequests {
+		return nil
+	}
+
+	if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID {
+		return nil
+	}
+
+	return issues_model.ErrNotValidReviewRequest{
+		Reason: "Doer can't remove reviewer",
+		UserID: doer.ID,
+		RepoID: issue.Repo.ID,
+	}
 }
 
 // IsValidTeamReviewRequest Check permission for ReviewRequest Team
@@ -181,11 +173,7 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
 		}
 	}
 
-	permission, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
-	if err != nil {
-		log.Error("Unable to GetUserRepoPermission for %-v in %-v#%d", doer, issue.Repo, issue.Index)
-		return err
-	}
+	canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue)
 
 	if isAdd {
 		if issue.Repo.IsPrivate {
@@ -200,30 +188,26 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
 			}
 		}
 
-		doerCanWrite := permission.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests)
-		if !doerCanWrite && doer.ID != issue.PosterID {
-			official, err := issues_model.IsOfficialReviewer(ctx, issue, doer)
-			if err != nil {
-				log.Error("Unable to Check if IsOfficialReviewer for %-v in %-v#%d", doer, issue.Repo, issue.Index)
-				return err
-			}
-			if !official {
-				return issues_model.ErrNotValidReviewRequest{
-					Reason: "Doer can't choose reviewer",
-					UserID: doer.ID,
-					RepoID: issue.Repo.ID,
-				}
-			}
+		if canDoerChangeReviewRequests {
+			return nil
 		}
-	} else if !permission.IsAdmin() {
+
 		return issues_model.ErrNotValidReviewRequest{
-			Reason: "Only admin users can remove team requests. Doer is not admin",
+			Reason: "Doer can't choose reviewer",
 			UserID: doer.ID,
 			RepoID: issue.Repo.ID,
 		}
 	}
 
-	return nil
+	if canDoerChangeReviewRequests {
+		return nil
+	}
+
+	return issues_model.ErrNotValidReviewRequest{
+		Reason: "Doer can't remove reviewer",
+		UserID: doer.ID,
+		RepoID: issue.Repo.ID,
+	}
 }
 
 // TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it.
@@ -242,16 +226,33 @@ func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *use
 		return nil, nil
 	}
 
+	return comment, teamReviewRequestNotify(ctx, issue, doer, reviewer, isAdd, comment)
+}
+
+func ReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewNotifers []*ReviewRequestNotifier) {
+	for _, reviewNotifer := range reviewNotifers {
+		if reviewNotifer.Reviwer != nil {
+			notify_service.PullRequestReviewRequest(ctx, issue.Poster, issue, reviewNotifer.Reviwer, reviewNotifer.IsAdd, reviewNotifer.Comment)
+		} else if reviewNotifer.ReviewTeam != nil {
+			if err := teamReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifer.ReviewTeam, reviewNotifer.IsAdd, reviewNotifer.Comment); err != nil {
+				log.Error("teamReviewRequestNotify: %v", err)
+			}
+		}
+	}
+}
+
+// teamReviewRequestNotify notify all user in this team
+func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool, comment *issues_model.Comment) error {
 	// notify all user in this team
 	if err := comment.LoadIssue(ctx); err != nil {
-		return nil, err
+		return err
 	}
 
 	members, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
 		TeamID: reviewer.ID,
 	})
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	for _, member := range members {
@@ -262,5 +263,52 @@ func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *use
 		notify_service.PullRequestReviewRequest(ctx, doer, issue, member, isAdd, comment)
 	}
 
-	return comment, err
+	return err
+}
+
+// CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR
+func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue) bool {
+	// The poster of the PR can change the reviewers
+	if doer.ID == issue.PosterID {
+		return true
+	}
+
+	// The owner of the repo can change the reviewers
+	if doer.ID == repo.OwnerID {
+		return true
+	}
+
+	// Collaborators of the repo can change the reviewers
+	isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, doer.ID)
+	if err != nil {
+		log.Error("IsCollaborator: %v", err)
+		return false
+	}
+	if isCollaborator {
+		return true
+	}
+
+	// If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers
+	if repo.Owner.IsOrganization() {
+		teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
+		if err != nil {
+			log.Error("GetTeamsWithAccessToRepo: %v", err)
+			return false
+		}
+		for _, team := range teams {
+			if !team.UnitEnabled(ctx, unit.TypePullRequests) {
+				continue
+			}
+			isMember, err := organization.IsTeamMember(ctx, repo.OwnerID, team.ID, doer.ID)
+			if err != nil {
+				log.Error("IsTeamMember: %v", err)
+				continue
+			}
+			if isMember {
+				return true
+			}
+		}
+	}
+
+	return false
 }
diff --git a/services/issue/comments.go b/services/issue/comments.go
index 8d8c575c14..d68623aff6 100644
--- a/services/issue/comments.go
+++ b/services/issue/comments.go
@@ -9,6 +9,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
+	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -21,6 +22,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
 		return fmt.Errorf("cannot create reference with empty commit SHA")
 	}
 
+	if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
+		if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
+			return user_model.ErrBlockedUser
+		}
+	}
+
 	// Check if same reference from same commit has already existed.
 	has, err := db.GetEngine(ctx).Get(&issues_model.Comment{
 		Type:      issues_model.CommentTypeCommitRef,
@@ -46,6 +53,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
 
 // CreateIssueComment creates a plain issue comment.
 func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) {
+	if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
+		if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
+			return nil, user_model.ErrBlockedUser
+		}
+	}
+
 	comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
 		Type:        issues_model.CommentTypeComment,
 		Doer:        doer,
@@ -70,6 +83,19 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
 
 // UpdateComment updates information of comment.
 func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_model.User, oldContent string) error {
+	if err := c.LoadIssue(ctx); err != nil {
+		return err
+	}
+	if err := c.Issue.LoadRepo(ctx); err != nil {
+		return err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, doer, c.Issue.PosterID, c.Issue.Repo.OwnerID) {
+		if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, c.Issue.Repo, doer); !isAdmin {
+			return user_model.ErrBlockedUser
+		}
+	}
+
 	needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport()
 	if needsContentHistory {
 		hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID)
diff --git a/services/issue/commit.go b/services/issue/commit.go
index e493a03211..0a59088d12 100644
--- a/services/issue/commit.go
+++ b/services/issue/commit.go
@@ -5,6 +5,7 @@ package issue
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"html"
 	"net/url"
@@ -160,6 +161,9 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m
 
 			message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0]))
 			if err = CreateRefComment(ctx, doer, refRepo, refIssue, message, c.Sha1); err != nil {
+				if errors.Is(err, user_model.ErrBlockedUser) {
+					continue
+				}
 				return err
 			}
 
diff --git a/services/issue/content.go b/services/issue/content.go
index 6e56714ddf..2f9bee806a 100644
--- a/services/issue/content.go
+++ b/services/issue/content.go
@@ -7,12 +7,23 @@ import (
 	"context"
 
 	issues_model "code.gitea.io/gitea/models/issues"
+	access_model "code.gitea.io/gitea/models/perm/access"
 	user_model "code.gitea.io/gitea/models/user"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
 
 // ChangeContent changes issue content, as the given user.
-func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) (err error) {
+func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) error {
+	if err := issue.LoadRepo(ctx); err != nil {
+		return err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+		if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
+			return user_model.ErrBlockedUser
+		}
+	}
+
 	oldContent := issue.Content
 
 	if err := issues_model.ChangeIssueContent(ctx, issue, doer, content); err != nil {
diff --git a/services/issue/issue.go b/services/issue/issue.go
index b1f418c32e..c7fa9f3300 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -15,21 +15,40 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	system_model "code.gitea.io/gitea/models/system"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/storage"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
 
 // NewIssue creates new issue with labels for repository.
-func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
-	if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
+func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, projectID int64) error {
+	if err := issue.LoadPoster(ctx); err != nil {
 		return err
 	}
 
-	for _, assigneeID := range assigneeIDs {
-		if _, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, true); err != nil {
+	if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
+		return user_model.ErrBlockedUser
+	}
+
+	if err := db.WithTx(ctx, func(ctx context.Context) error {
+		if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
 			return err
 		}
+		for _, assigneeID := range assigneeIDs {
+			if _, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, true); err != nil {
+				return err
+			}
+		}
+		if projectID > 0 {
+			if err := issues_model.ChangeProjectAssign(ctx, issue, issue.Poster, projectID); err != nil {
+				return err
+			}
+		}
+		return nil
+	}); err != nil {
+		return err
 	}
 
 	mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content)
@@ -57,17 +76,31 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
 		return nil
 	}
 
+	if err := issue.LoadRepo(ctx); err != nil {
+		return err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+		if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
+			return user_model.ErrBlockedUser
+		}
+	}
+
 	if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
 		return err
 	}
 
+	var reviewNotifers []*ReviewRequestNotifier
 	if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
-		if err := issues_model.PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest); err != nil {
-			return err
+		var err error
+		reviewNotifers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest)
+		if err != nil {
+			log.Error("PullRequestCodeOwnersReview: %v", err)
 		}
 	}
 
 	notify_service.IssueChangeTitle(ctx, doer, issue, oldTitle)
+	ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifers)
 
 	return nil
 }
@@ -93,31 +126,25 @@ func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_m
 // Pass one or more user logins to replace the set of assignees on this Issue.
 // Send an empty array ([]) to clear all assignees from the Issue.
 func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
-	var allNewAssignees []*user_model.User
+	uniqueAssignees := container.SetOf(multipleAssignees...)
 
 	// Keep the old assignee thingy for compatibility reasons
 	if oneAssignee != "" {
-		// Prevent double adding assignees
-		var isDouble bool
-		for _, assignee := range multipleAssignees {
-			if assignee == oneAssignee {
-				isDouble = true
-				break
-			}
-		}
-
-		if !isDouble {
-			multipleAssignees = append(multipleAssignees, oneAssignee)
-		}
+		uniqueAssignees.Add(oneAssignee)
 	}
 
 	// Loop through all assignees to add them
-	for _, assigneeName := range multipleAssignees {
+	allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees))
+	for _, assigneeName := range uniqueAssignees.Values() {
 		assignee, err := user_model.GetUserByName(ctx, assigneeName)
 		if err != nil {
 			return err
 		}
 
+		if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) {
+			return user_model.ErrBlockedUser
+		}
+
 		allNewAssignees = append(allNewAssignees, assignee)
 	}
 
diff --git a/services/issue/pull.go b/services/issue/pull.go
new file mode 100644
index 0000000000..b7b63a7024
--- /dev/null
+++ b/services/issue/pull.go
@@ -0,0 +1,147 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	issues_model "code.gitea.io/gitea/models/issues"
+	org_model "code.gitea.io/gitea/models/organization"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+func getMergeBase(repo *git.Repository, pr *issues_model.PullRequest, baseBranch, headBranch string) (string, error) {
+	// Add a temporary remote
+	tmpRemote := fmt.Sprintf("mergebase-%d-%d", pr.ID, time.Now().UnixNano())
+	if err := repo.AddRemote(tmpRemote, repo.Path, false); err != nil {
+		return "", fmt.Errorf("AddRemote: %w", err)
+	}
+	defer func() {
+		if err := repo.RemoveRemote(tmpRemote); err != nil {
+			log.Error("getMergeBase: RemoveRemote: %v", err)
+		}
+	}()
+
+	mergeBase, _, err := repo.GetMergeBase(tmpRemote, baseBranch, headBranch)
+	return mergeBase, err
+}
+
+type ReviewRequestNotifier struct {
+	Comment    *issues_model.Comment
+	IsAdd      bool
+	Reviwer    *user_model.User
+	ReviewTeam *org_model.Team
+}
+
+func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
+	files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
+
+	if pr.IsWorkInProgress(ctx) {
+		return nil, nil
+	}
+
+	if err := pr.LoadHeadRepo(ctx); err != nil {
+		return nil, err
+	}
+
+	if pr.HeadRepo.IsFork {
+		return nil, nil
+	}
+
+	if err := pr.LoadBaseRepo(ctx); err != nil {
+		return nil, err
+	}
+
+	repo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
+	if err != nil {
+		return nil, err
+	}
+	defer repo.Close()
+
+	commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch)
+	if err != nil {
+		return nil, err
+	}
+
+	var data string
+	for _, file := range files {
+		if blob, err := commit.GetBlobByPath(file); err == nil {
+			data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
+			if err == nil {
+				break
+			}
+		}
+	}
+
+	rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data)
+
+	// get the mergebase
+	mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
+	if err != nil {
+		return nil, err
+	}
+
+	// https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed
+	// between the merge base and the head commit but not the base branch and the head commit
+	changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.GetGitRefName())
+	if err != nil {
+		return nil, err
+	}
+
+	uniqUsers := make(map[int64]*user_model.User)
+	uniqTeams := make(map[string]*org_model.Team)
+	for _, rule := range rules {
+		for _, f := range changedFiles {
+			if (rule.Rule.MatchString(f) && !rule.Negative) || (!rule.Rule.MatchString(f) && rule.Negative) {
+				for _, u := range rule.Users {
+					uniqUsers[u.ID] = u
+				}
+				for _, t := range rule.Teams {
+					uniqTeams[fmt.Sprintf("%d/%d", t.OrgID, t.ID)] = t
+				}
+			}
+		}
+	}
+
+	notifiers := make([]*ReviewRequestNotifier, 0, len(uniqUsers)+len(uniqTeams))
+
+	if err := issue.LoadPoster(ctx); err != nil {
+		return nil, err
+	}
+
+	for _, u := range uniqUsers {
+		if u.ID != issue.Poster.ID {
+			comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster)
+			if err != nil {
+				log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
+				return nil, err
+			}
+			notifiers = append(notifiers, &ReviewRequestNotifier{
+				Comment: comment,
+				IsAdd:   true,
+				Reviwer: u,
+			})
+		}
+	}
+	for _, t := range uniqTeams {
+		comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster)
+		if err != nil {
+			log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
+			return nil, err
+		}
+		notifiers = append(notifiers, &ReviewRequestNotifier{
+			Comment:    comment,
+			IsAdd:      true,
+			ReviewTeam: t,
+		})
+	}
+
+	return notifiers, nil
+}
diff --git a/services/issue/reaction.go b/services/issue/reaction.go
new file mode 100644
index 0000000000..deb99169e1
--- /dev/null
+++ b/services/issue/reaction.go
@@ -0,0 +1,50 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+	"context"
+
+	issues_model "code.gitea.io/gitea/models/issues"
+	user_model "code.gitea.io/gitea/models/user"
+)
+
+// CreateIssueReaction creates a reaction on an issue.
+func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, content string) (*issues_model.Reaction, error) {
+	if err := issue.LoadRepo(ctx); err != nil {
+		return nil, err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+		return nil, user_model.ErrBlockedUser
+	}
+
+	return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
+		Type:    content,
+		DoerID:  doer.ID,
+		IssueID: issue.ID,
+	})
+}
+
+// CreateCommentReaction creates a reaction on a comment.
+func CreateCommentReaction(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) {
+	if err := comment.LoadIssue(ctx); err != nil {
+		return nil, err
+	}
+
+	if err := comment.Issue.LoadRepo(ctx); err != nil {
+		return nil, err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, doer, comment.Issue.PosterID, comment.Issue.Repo.OwnerID, comment.PosterID) {
+		return nil, user_model.ErrBlockedUser
+	}
+
+	return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
+		Type:      content,
+		DoerID:    doer.ID,
+		IssueID:   comment.Issue.ID,
+		CommentID: comment.ID,
+	})
+}
diff --git a/models/issues/reaction_test.go b/services/issue/reaction_test.go
similarity index 65%
rename from models/issues/reaction_test.go
rename to services/issue/reaction_test.go
index 5dc8e1a5f3..7734860fc0 100644
--- a/models/issues/reaction_test.go
+++ b/services/issue/reaction_test.go
@@ -1,7 +1,7 @@
 // Copyright 2017 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
-package issues_test
+package issue
 
 import (
 	"testing"
@@ -16,13 +16,13 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
+func addReaction(t *testing.T, doer *user_model.User, issue *issues_model.Issue, comment *issues_model.Comment, content string) {
 	var reaction *issues_model.Reaction
 	var err error
-	if commentID == 0 {
-		reaction, err = issues_model.CreateIssueReaction(db.DefaultContext, doerID, issueID, content)
+	if comment == nil {
+		reaction, err = CreateIssueReaction(db.DefaultContext, doer, issue, content)
 	} else {
-		reaction, err = issues_model.CreateCommentReaction(db.DefaultContext, doerID, issueID, commentID, content)
+		reaction, err = CreateCommentReaction(db.DefaultContext, doer, comment, content)
 	}
 	assert.NoError(t, err)
 	assert.NotNil(t, reaction)
@@ -32,32 +32,26 @@ func TestIssueAddReaction(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
 
-	var issue1ID int64 = 1
+	addReaction(t, user1, issue, nil, "heart")
 
-	addReaction(t, user1.ID, issue1ID, 0, "heart")
-
-	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
 }
 
 func TestIssueAddDuplicateReaction(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
 
-	var issue1ID int64 = 1
+	addReaction(t, user1, issue, nil, "heart")
 
-	addReaction(t, user1.ID, issue1ID, 0, "heart")
-
-	reaction, err := issues_model.CreateReaction(db.DefaultContext, &issues_model.ReactionOptions{
-		DoerID:  user1.ID,
-		IssueID: issue1ID,
-		Type:    "heart",
-	})
+	reaction, err := CreateIssueReaction(db.DefaultContext, user1, issue, "heart")
 	assert.Error(t, err)
 	assert.Equal(t, issues_model.ErrReactionAlreadyExist{Reaction: "heart"}, err)
 
-	existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+	existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
 	assert.Equal(t, existingR.ID, reaction.ID)
 }
 
@@ -65,15 +59,14 @@ func TestIssueDeleteReaction(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
 
-	var issue1ID int64 = 1
+	addReaction(t, user1, issue, nil, "heart")
 
-	addReaction(t, user1.ID, issue1ID, 0, "heart")
-
-	err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue1ID, "heart")
+	err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue.ID, "heart")
 	assert.NoError(t, err)
 
-	unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+	unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
 }
 
 func TestIssueReactionCount(t *testing.T) {
@@ -87,19 +80,19 @@ func TestIssueReactionCount(t *testing.T) {
 	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 	ghost := user_model.NewGhostUser()
 
-	var issueID int64 = 2
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 
-	addReaction(t, user1.ID, issueID, 0, "heart")
-	addReaction(t, user2.ID, issueID, 0, "heart")
-	addReaction(t, org3.ID, issueID, 0, "heart")
-	addReaction(t, org3.ID, issueID, 0, "+1")
-	addReaction(t, user4.ID, issueID, 0, "+1")
-	addReaction(t, user4.ID, issueID, 0, "heart")
-	addReaction(t, ghost.ID, issueID, 0, "-1")
+	addReaction(t, user1, issue, nil, "heart")
+	addReaction(t, user2, issue, nil, "heart")
+	addReaction(t, org3, issue, nil, "heart")
+	addReaction(t, org3, issue, nil, "+1")
+	addReaction(t, user4, issue, nil, "+1")
+	addReaction(t, user4, issue, nil, "heart")
+	addReaction(t, ghost, issue, nil, "-1")
 
 	reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
-		IssueID: issueID,
+		IssueID: issue.ID,
 	})
 	assert.NoError(t, err)
 	assert.Len(t, reactionsList, 7)
@@ -122,13 +115,11 @@ func TestIssueCommentAddReaction(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
 
-	var issue1ID int64 = 1
-	var comment1ID int64 = 1
+	addReaction(t, user1, nil, comment, "heart")
 
-	addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
-
-	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
 }
 
 func TestIssueCommentDeleteReaction(t *testing.T) {
@@ -139,17 +130,16 @@ func TestIssueCommentDeleteReaction(t *testing.T) {
 	org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
 	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 
-	var issue1ID int64 = 1
-	var comment1ID int64 = 1
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
 
-	addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
-	addReaction(t, user2.ID, issue1ID, comment1ID, "heart")
-	addReaction(t, org3.ID, issue1ID, comment1ID, "heart")
-	addReaction(t, user4.ID, issue1ID, comment1ID, "+1")
+	addReaction(t, user1, nil, comment, "heart")
+	addReaction(t, user2, nil, comment, "heart")
+	addReaction(t, org3, nil, comment, "heart")
+	addReaction(t, user4, nil, comment, "+1")
 
 	reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
-		IssueID:   issue1ID,
-		CommentID: comment1ID,
+		IssueID:   comment.IssueID,
+		CommentID: comment.ID,
 	})
 	assert.NoError(t, err)
 	assert.Len(t, reactionsList, 4)
@@ -163,12 +153,10 @@ func TestIssueCommentReactionCount(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
 
-	var issue1ID int64 = 1
-	var comment1ID int64 = 1
+	addReaction(t, user1, nil, comment, "heart")
+	assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, comment.IssueID, comment.ID, "heart"))
 
-	addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
-	assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, issue1ID, comment1ID, "heart"))
-
-	unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
+	unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
 }
diff --git a/services/issue/template.go b/services/issue/template.go
index b6ae077987..dd9d015f0f 100644
--- a/services/issue/template.go
+++ b/services/issue/template.go
@@ -109,21 +109,23 @@ func IsTemplateConfig(path string) bool {
 	return false
 }
 
-// GetTemplatesFromDefaultBranch checks for issue templates in the repo's default branch,
-// returns valid templates and the errors of invalid template files.
-func GetTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) ([]*api.IssueTemplate, map[string]error) {
-	var issueTemplates []*api.IssueTemplate
-
+// ParseTemplatesFromDefaultBranch parses the issue templates in the repo's default branch,
+// returns valid templates and the errors of invalid template files (the errors map is guaranteed to be non-nil).
+func ParseTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) (ret struct {
+	IssueTemplates []*api.IssueTemplate
+	TemplateErrors map[string]error
+},
+) {
+	ret.TemplateErrors = map[string]error{}
 	if repo.IsEmpty {
-		return issueTemplates, nil
+		return ret
 	}
 
 	commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
 	if err != nil {
-		return issueTemplates, nil
+		return ret
 	}
 
-	invalidFiles := map[string]error{}
 	for _, dirName := range templateDirCandidates {
 		tree, err := commit.SubTree(dirName)
 		if err != nil {
@@ -133,7 +135,7 @@ func GetTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repositor
 		entries, err := tree.ListEntries()
 		if err != nil {
 			log.Debug("list entries in %s: %v", dirName, err)
-			return issueTemplates, nil
+			return ret
 		}
 		for _, entry := range entries {
 			if !template.CouldBe(entry.Name()) {
@@ -141,16 +143,16 @@ func GetTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repositor
 			}
 			fullName := path.Join(dirName, entry.Name())
 			if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil {
-				invalidFiles[fullName] = err
+				ret.TemplateErrors[fullName] = err
 			} else {
 				if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
 					it.Ref = git.BranchPrefix + it.Ref
 				}
-				issueTemplates = append(issueTemplates, it)
+				ret.IssueTemplates = append(ret.IssueTemplates, it)
 			}
 		}
 	}
-	return issueTemplates, invalidFiles
+	return ret
 }
 
 // GetTemplateConfigFromDefaultBranch returns the issue config for this repo.
@@ -179,8 +181,8 @@ func GetTemplateConfigFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repo
 }
 
 func HasTemplatesOrContactLinks(repo *repo.Repository, gitRepo *git.Repository) bool {
-	ret, _ := GetTemplatesFromDefaultBranch(repo, gitRepo)
-	if len(ret) > 0 {
+	ret := ParseTemplatesFromDefaultBranch(repo, gitRepo)
+	if len(ret.IssueTemplates) > 0 {
 		return true
 	}
 
diff --git a/services/lfs/locks.go b/services/lfs/locks.go
index 08d7432656..2a362b1c0d 100644
--- a/services/lfs/locks.go
+++ b/services/lfs/locks.go
@@ -11,12 +11,12 @@ import (
 	auth_model "code.gitea.io/gitea/models/auth"
 	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	lfs_module "code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 )
 
diff --git a/services/lfs/server.go b/services/lfs/server.go
index 62134b7d60..706be0d080 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -5,6 +5,7 @@ package lfs
 
 import (
 	stdCtx "context"
+	"crypto/sha256"
 	"encoding/base64"
 	"encoding/hex"
 	"errors"
@@ -25,15 +26,14 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
 	lfs_module "code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/golang-jwt/jwt/v5"
-	"github.com/minio/sha256-simd"
 )
 
 // requestContext contain variables from the HTTP request.
diff --git a/services/mailer/incoming/incoming_handler.go b/services/mailer/incoming/incoming_handler.go
index 9682c52456..dc0b539822 100644
--- a/services/mailer/incoming/incoming_handler.go
+++ b/services/mailer/incoming/incoming_handler.go
@@ -14,9 +14,9 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	attachment_service "code.gitea.io/gitea/services/attachment"
+	"code.gitea.io/gitea/services/context/upload"
 	issue_service "code.gitea.io/gitea/services/issue"
 	incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
 	"code.gitea.io/gitea/services/mailer/token"
@@ -130,6 +130,7 @@ func (h *ReplyHandler) Handle(ctx context.Context, content *MailContent, doer *u
 				false, // not pending review but a single review
 				comment.ReviewID,
 				"",
+				nil,
 			)
 			if err != nil {
 				return fmt.Errorf("CreateCodeComment failed: %w", err)
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index ca27336f92..a63ba7a52a 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -94,7 +94,7 @@ func SendActivateAccountMail(locale translation.Locale, u *user_model.User) {
 		// No mail service configured
 		return
 	}
-	sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.activate_account"), "activate account")
+	sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account")
 }
 
 // SendResetPasswordMail sends a password reset mail to the user
@@ -104,7 +104,7 @@ func SendResetPasswordMail(u *user_model.User) {
 		return
 	}
 	locale := translation.NewLocale(u.Language)
-	sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.reset_password"), "recover account")
+	sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account")
 }
 
 // SendActivateEmailMail sends confirmation email to confirm new email address
@@ -130,7 +130,7 @@ func SendActivateEmailMail(u *user_model.User, email string) {
 		return
 	}
 
-	msg := NewMessage(email, locale.Tr("mail.activate_email"), content.String())
+	msg := NewMessage(email, locale.TrString("mail.activate_email"), content.String())
 	msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID)
 
 	SendAsync(msg)
@@ -158,7 +158,7 @@ func SendRegisterNotifyMail(u *user_model.User) {
 		return
 	}
 
-	msg := NewMessage(u.Email, locale.Tr("mail.register_notify"), content.String())
+	msg := NewMessage(u.Email, locale.TrString("mail.register_notify"), content.String())
 	msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID)
 
 	SendAsync(msg)
@@ -173,7 +173,7 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository)
 	locale := translation.NewLocale(u.Language)
 	repoName := repo.FullName()
 
-	subject := locale.Tr("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName)
+	subject := locale.TrString("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName)
 	data := map[string]any{
 		"locale":   locale,
 		"Subject":  subject,
@@ -222,7 +222,8 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
 	body, err := markdown.RenderString(&markup.RenderContext{
 		Ctx: ctx,
 		Links: markup.Links{
-			Base: ctx.Issue.Repo.HTMLURL(),
+			AbsolutePrefix: true,
+			Base:           ctx.Issue.Repo.HTMLURL(),
 		},
 		Metas: ctx.Issue.Repo.ComposeMetas(ctx),
 	}, ctx.Content)
diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go
index 5e8e5b6af3..6682774a04 100644
--- a/services/mailer/mail_release.go
+++ b/services/mailer/mail_release.go
@@ -68,7 +68,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
 		return
 	}
 
-	subject := locale.Tr("mail.release.new.subject", rel.TagName, rel.Repo.FullName())
+	subject := locale.TrString("mail.release.new.subject", rel.TagName, rel.Repo.FullName())
 	mailMeta := map[string]any{
 		"locale":   locale,
 		"Release":  rel,
diff --git a/services/mailer/mail_repo.go b/services/mailer/mail_repo.go
index b89dcd43b5..e0d55bb120 100644
--- a/services/mailer/mail_repo.go
+++ b/services/mailer/mail_repo.go
@@ -56,11 +56,11 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
 		content bytes.Buffer
 	)
 
-	destination := locale.Tr("mail.repo.transfer.to_you")
-	subject := locale.Tr("mail.repo.transfer.subject_to_you", doer.DisplayName(), repo.FullName())
+	destination := locale.TrString("mail.repo.transfer.to_you")
+	subject := locale.TrString("mail.repo.transfer.subject_to_you", doer.DisplayName(), repo.FullName())
 	if newOwner.IsOrganization() {
 		destination = newOwner.DisplayName()
-		subject = locale.Tr("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination)
+		subject = locale.TrString("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination)
 	}
 
 	data := map[string]any{
diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go
index ab32beefac..ceecefa50f 100644
--- a/services/mailer/mail_team_invite.go
+++ b/services/mailer/mail_team_invite.go
@@ -50,7 +50,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod
 		inviteURL = fmt.Sprintf("%suser/login?redirect_to=%s", setting.AppURL, inviteRedirect)
 	}
 
-	subject := locale.Tr("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName())
+	subject := locale.TrString("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName())
 	mailMeta := map[string]any{
 		"locale":       locale,
 		"Inviter":      inviter,
diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go
index e300aeccb0..d87c57ffe7 100644
--- a/services/mailer/mail_test.go
+++ b/services/mailer/mail_test.go
@@ -8,6 +8,8 @@ import (
 	"context"
 	"fmt"
 	"html/template"
+	"io"
+	"mime/quotedprintable"
 	"regexp"
 	"strings"
 	"testing"
@@ -19,6 +21,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/stretchr/testify/assert"
@@ -67,6 +70,12 @@ func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Re
 func TestComposeIssueCommentMessage(t *testing.T) {
 	doer, _, issue, comment := prepareMailerTest(t)
 
+	markup.Init(&markup.ProcessorHelper{
+		IsUsernameMentionable: func(ctx context.Context, username string) bool {
+			return username == doer.Name
+		},
+	})
+
 	setting.IncomingEmail.Enabled = true
 	defer func() { setting.IncomingEmail.Enabled = false }()
 
@@ -77,7 +86,8 @@ func TestComposeIssueCommentMessage(t *testing.T) {
 	msgs, err := composeIssueCommentMessages(&mailCommentContext{
 		Context: context.TODO(), // TODO: use a correct context
 		Issue:   issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
-		Content: "test body", Comment: comment,
+		Content: fmt.Sprintf("test @%s %s#%d body", doer.Name, issue.Repo.FullName(), issue.Index),
+		Comment: comment,
 	}, "en-US", recipients, false, "issue comment")
 	assert.NoError(t, err)
 	assert.Len(t, msgs, 2)
@@ -96,6 +106,20 @@ func TestComposeIssueCommentMessage(t *testing.T) {
 	assert.Equal(t, "<user2/repo1/issues/1/comment/2@localhost>", gomailMsg.GetHeader("Message-ID")[0], "Message-ID header doesn't match")
 	assert.Equal(t, "<mailto:"+replyTo+">", gomailMsg.GetHeader("List-Post")[0])
 	assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 2) // url + mailto
+
+	var buf bytes.Buffer
+	gomailMsg.WriteTo(&buf)
+
+	b, err := io.ReadAll(quotedprintable.NewReader(&buf))
+	assert.NoError(t, err)
+
+	// text/plain
+	assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, doer.HTMLURL()))
+	assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, issue.HTMLURL()))
+
+	// text/html
+	assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, doer.HTMLURL()))
+	assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, issue.HTMLURL()))
 }
 
 func TestComposeIssueMessage(t *testing.T) {
diff --git a/services/mailer/token/token.go b/services/mailer/token/token.go
index aa7b567188..8a5a762d6b 100644
--- a/services/mailer/token/token.go
+++ b/services/mailer/token/token.go
@@ -6,14 +6,13 @@ package token
 import (
 	"context"
 	crypto_hmac "crypto/hmac"
+	"crypto/sha256"
 	"encoding/base32"
 	"fmt"
 	"time"
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/util"
-
-	"github.com/minio/sha256-simd"
 )
 
 // A token is a verifiable container describing an action.
diff --git a/services/markup/main_test.go b/services/markup/main_test.go
index 89fe3e7e34..5553ebc058 100644
--- a/services/markup/main_test.go
+++ b/services/markup/main_test.go
@@ -11,6 +11,6 @@ import (
 
 func TestMain(m *testing.M) {
 	unittest.MainTest(m, &unittest.TestOptions{
-		FixtureFiles: []string{"user.yml"},
+		FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml"},
 	})
 }
diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go
index 3551f85c46..68487fb8db 100644
--- a/services/markup/processorhelper.go
+++ b/services/markup/processorhelper.go
@@ -7,13 +7,15 @@ import (
 	"context"
 
 	"code.gitea.io/gitea/models/user"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
+	gitea_context "code.gitea.io/gitea/services/context"
 )
 
 func ProcessorHelper() *markup.ProcessorHelper {
 	return &markup.ProcessorHelper{
 		ElementDir: "auto", // set dir="auto" for necessary (eg: <p>, <h?>, etc) tags
+
+		RenderRepoFileCodePreview: renderRepoFileCodePreview,
 		IsUsernameMentionable: func(ctx context.Context, username string) bool {
 			mentionedUser, err := user.GetUserByName(ctx, username)
 			if err != nil {
diff --git a/services/markup/processorhelper_codepreview.go b/services/markup/processorhelper_codepreview.go
new file mode 100644
index 0000000000..ef95046128
--- /dev/null
+++ b/services/markup/processorhelper_codepreview.go
@@ -0,0 +1,117 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"html/template"
+	"strings"
+
+	"code.gitea.io/gitea/models/perm/access"
+	"code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unit"
+	"code.gitea.io/gitea/modules/charset"
+	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/indexer/code"
+	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/setting"
+	gitea_context "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/repository/files"
+)
+
+func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
+	opts.LineStop = max(opts.LineStop, opts.LineStart)
+	lineCount := opts.LineStop - opts.LineStart + 1
+	if lineCount <= 0 || lineCount > 140 /* GitHub at most show 140 lines */ {
+		lineCount = 10
+		opts.LineStop = opts.LineStart + lineCount
+	}
+
+	dbRepo, err := repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName)
+	if err != nil {
+		return "", err
+	}
+
+	webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context)
+	if !ok {
+		return "", fmt.Errorf("context is not a web context")
+	}
+	doer := webCtx.Doer
+
+	perms, err := access.GetUserRepoPermission(ctx, dbRepo, doer)
+	if err != nil {
+		return "", err
+	}
+	if !perms.CanRead(unit.TypeCode) {
+		return "", fmt.Errorf("no permission")
+	}
+
+	gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo)
+	if err != nil {
+		return "", err
+	}
+	defer gitRepo.Close()
+
+	commit, err := gitRepo.GetCommit(opts.CommitID)
+	if err != nil {
+		return "", err
+	}
+
+	language, _ := files.TryGetContentLanguage(gitRepo, opts.CommitID, opts.FilePath)
+	blob, err := commit.GetBlobByPath(opts.FilePath)
+	if err != nil {
+		return "", err
+	}
+
+	if blob.Size() > setting.UI.MaxDisplayFileSize {
+		return "", fmt.Errorf("file is too large")
+	}
+
+	dataRc, err := blob.DataAsync()
+	if err != nil {
+		return "", err
+	}
+	defer dataRc.Close()
+
+	reader := bufio.NewReader(dataRc)
+	for i := 1; i < opts.LineStart; i++ {
+		if _, err = reader.ReadBytes('\n'); err != nil {
+			return "", err
+		}
+	}
+
+	lineNums := make([]int, 0, lineCount)
+	lineCodes := make([]string, 0, lineCount)
+	for i := opts.LineStart; i <= opts.LineStop; i++ {
+		if line, err := reader.ReadString('\n'); err != nil && line == "" {
+			break
+		} else {
+			lineNums = append(lineNums, i)
+			lineCodes = append(lineCodes, line)
+		}
+	}
+	realLineStop := max(opts.LineStart, opts.LineStart+len(lineNums)-1)
+	highlightLines := code.HighlightSearchResultCode(opts.FilePath, language, lineNums, strings.Join(lineCodes, ""))
+
+	escapeStatus := &charset.EscapeStatus{}
+	lineEscapeStatus := make([]*charset.EscapeStatus, len(highlightLines))
+	for i, hl := range highlightLines {
+		lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, webCtx.Base.Locale, charset.RuneNBSP)
+		escapeStatus = escapeStatus.Or(lineEscapeStatus[i])
+	}
+
+	return webCtx.RenderToHTML("base/markup_codepreview", map[string]any{
+		"FullURL":          opts.FullURL,
+		"FilePath":         opts.FilePath,
+		"LineStart":        opts.LineStart,
+		"LineStop":         realLineStop,
+		"RepoLink":         dbRepo.Link(),
+		"CommitID":         opts.CommitID,
+		"HighlightLines":   highlightLines,
+		"EscapeStatus":     escapeStatus,
+		"LineEscapeStatus": lineEscapeStatus,
+	})
+}
diff --git a/services/markup/processorhelper_codepreview_test.go b/services/markup/processorhelper_codepreview_test.go
new file mode 100644
index 0000000000..154e4e8e44
--- /dev/null
+++ b/services/markup/processorhelper_codepreview_test.go
@@ -0,0 +1,83 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/templates"
+	"code.gitea.io/gitea/services/contexttest"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestProcessorHelperCodePreview(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+	htm, err := renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+		FullURL:   "http://full",
+		OwnerName: "user2",
+		RepoName:  "repo1",
+		CommitID:  "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+		FilePath:  "/README.md",
+		LineStart: 1,
+		LineStop:  2,
+	})
+	assert.NoError(t, err)
+	assert.Equal(t, `<div class="code-preview-container file-content">
+	<div class="code-preview-header">
+		<a href="http://full" class="muted" rel="nofollow">/README.md</a>
+		repo.code_preview_line_from_to:1,2,<a href="/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow">65f1bf27bc</a>
+	</div>
+	<table class="file-view">
+		<tbody><tr>
+				<td class="lines-num"><span data-line-number="1"></span></td>
+				<td class="lines-code chroma"><div class="code-inner"><span class="gh"># repo1</div></td>
+			</tr><tr>
+				<td class="lines-num"><span data-line-number="2"></span></td>
+				<td class="lines-code chroma"><div class="code-inner"></span><span class="gh"></span></div></td>
+			</tr></tbody>
+	</table>
+</div>
+`, string(htm))
+
+	ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+	htm, err = renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+		FullURL:   "http://full",
+		OwnerName: "user2",
+		RepoName:  "repo1",
+		CommitID:  "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+		FilePath:  "/README.md",
+		LineStart: 1,
+	})
+	assert.NoError(t, err)
+	assert.Equal(t, `<div class="code-preview-container file-content">
+	<div class="code-preview-header">
+		<a href="http://full" class="muted" rel="nofollow">/README.md</a>
+		repo.code_preview_line_in:1,<a href="/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow">65f1bf27bc</a>
+	</div>
+	<table class="file-view">
+		<tbody><tr>
+				<td class="lines-num"><span data-line-number="1"></span></td>
+				<td class="lines-code chroma"><div class="code-inner"><span class="gh"># repo1</div></td>
+			</tr></tbody>
+	</table>
+</div>
+`, string(htm))
+
+	ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+	_, err = renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+		FullURL:   "http://full",
+		OwnerName: "user15",
+		RepoName:  "big_test_private_1",
+		CommitID:  "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+		FilePath:  "/README.md",
+		LineStart: 1,
+		LineStop:  10,
+	})
+	assert.ErrorContains(t, err, "no permission")
+}
diff --git a/services/markup/processorhelper_test.go b/services/markup/processorhelper_test.go
index ef8f562245..170edae0e0 100644
--- a/services/markup/processorhelper_test.go
+++ b/services/markup/processorhelper_test.go
@@ -12,8 +12,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/models/user"
-	gitea_context "code.gitea.io/gitea/modules/context"
-	"code.gitea.io/gitea/modules/contexttest"
+	gitea_context "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/services/migrations/gitbucket.go b/services/migrations/gitbucket.go
index 5f11555839..4fe9e30a39 100644
--- a/services/migrations/gitbucket.go
+++ b/services/migrations/gitbucket.go
@@ -72,6 +72,11 @@ func (g *GitBucketDownloader) LogString() string {
 // NewGitBucketDownloader creates a GitBucket downloader
 func NewGitBucketDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GitBucketDownloader {
 	githubDownloader := NewGithubDownloaderV3(ctx, baseURL, userName, password, token, repoOwner, repoName)
+	// Gitbucket 4.40 uses different internal hard-coded perPage values.
+	// Issues, PRs, and other major parts use 25.  Release page uses 10.
+	// Some API doesn't support paging yet.  Sounds difficult, but using
+	// minimum number among them worked out very well.
+	githubDownloader.maxPerPage = 10
 	githubDownloader.SkipReactions = true
 	githubDownloader.SkipReviews = true
 	return &GitBucketDownloader{
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 7b21d9f4d2..87691bf729 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -120,7 +120,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
 	r.DefaultBranch = repo.DefaultBranch
 	r.Description = repo.Description
 
-	r, err = repo_module.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{
+	r, err = repo_service.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{
 		RepoName:       g.repoName,
 		Description:    repo.Description,
 		OriginalURL:    repo.OriginalURL,
@@ -140,8 +140,18 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
 	if err != nil {
 		return err
 	}
-	g.gitRepo, err = gitrepo.OpenRepository(g.ctx, r)
-	return err
+	g.gitRepo, err = gitrepo.OpenRepository(g.ctx, g.repo)
+	if err != nil {
+		return err
+	}
+
+	// detect object format from git repository and update to database
+	objectFormat, err := g.gitRepo.GetObjectFormat()
+	if err != nil {
+		return err
+	}
+	g.repo.ObjectFormatName = objectFormat.Name()
+	return repo_model.UpdateRepositoryCols(g.ctx, g.repo, "object_format_name")
 }
 
 // Close closes this uploader
@@ -473,6 +483,10 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
 		}
 
 		switch cm.Type {
+		case issues_model.CommentTypeReopen:
+			cm.Content = ""
+		case issues_model.CommentTypeClose:
+			cm.Content = ""
 		case issues_model.CommentTypeAssignees:
 			if assigneeID, ok := comment.Meta["AssigneeID"].(int); ok {
 				cm.AssigneeID = int64(assigneeID)
@@ -482,11 +496,21 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
 			}
 		case issues_model.CommentTypeChangeTitle:
 			if comment.Meta["OldTitle"] != nil {
-				cm.OldTitle = fmt.Sprintf("%s", comment.Meta["OldTitle"])
+				cm.OldTitle = fmt.Sprint(comment.Meta["OldTitle"])
 			}
 			if comment.Meta["NewTitle"] != nil {
-				cm.NewTitle = fmt.Sprintf("%s", comment.Meta["NewTitle"])
+				cm.NewTitle = fmt.Sprint(comment.Meta["NewTitle"])
 			}
+		case issues_model.CommentTypeChangeTargetBranch:
+			if comment.Meta["OldRef"] != nil && comment.Meta["NewRef"] != nil {
+				cm.OldRef = fmt.Sprint(comment.Meta["OldRef"])
+				cm.NewRef = fmt.Sprint(comment.Meta["NewRef"])
+				cm.Content = ""
+			}
+		case issues_model.CommentTypeMergePull:
+			cm.Content = ""
+		case issues_model.CommentTypePRScheduledToAutoMerge, issues_model.CommentTypePRUnScheduledToAutoMerge:
+			cm.Content = ""
 		default:
 		}
 
@@ -894,7 +918,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
 				comment.UpdatedAt = comment.CreatedAt
 			}
 
-			objectFormat, _ := g.gitRepo.GetObjectFormat()
+			objectFormat := git.ObjectFormatFromName(g.repo.ObjectFormatName)
 			if !objectFormat.IsValid(comment.CommitID) {
 				log.Warn("Invalid comment CommitID[%s] on comment[%d] in PR #%d of %s/%s replaced with %s", comment.CommitID, pr.Index, g.repoOwner, g.repoName, headCommitID)
 				comment.CommitID = headCommitID
diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go
index c8102c6b8b..c9b9248098 100644
--- a/services/migrations/gitea_uploader_test.go
+++ b/services/migrations/gitea_uploader_test.go
@@ -23,9 +23,9 @@ import (
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
 	base "code.gitea.io/gitea/modules/migration"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
-	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -68,14 +68,14 @@ func TestGiteaUploadRepo(t *testing.T) {
 
 	milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 		RepoID:   repo.ID,
-		IsClosed: util.OptionalBoolFalse,
+		IsClosed: optional.Some(false),
 	})
 	assert.NoError(t, err)
 	assert.Len(t, milestones, 1)
 
 	milestones, err = db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
 		RepoID:   repo.ID,
-		IsClosed: util.OptionalBoolTrue,
+		IsClosed: optional.Some(true),
 	})
 	assert.NoError(t, err)
 	assert.Empty(t, milestones)
@@ -108,7 +108,7 @@ func TestGiteaUploadRepo(t *testing.T) {
 
 	issues, err := issues_model.Issues(db.DefaultContext, &issues_model.IssuesOptions{
 		RepoIDs:  []int64{repo.ID},
-		IsPull:   util.OptionalBoolFalse,
+		IsPull:   optional.Some(false),
 		SortType: "oldest",
 	})
 	assert.NoError(t, err)
diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go
index 3db10465fc..bbc44e958a 100644
--- a/services/migrations/gitlab.go
+++ b/services/migrations/gitlab.go
@@ -11,9 +11,11 @@ import (
 	"net/http"
 	"net/url"
 	"path"
+	"regexp"
 	"strings"
 	"time"
 
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
 	base "code.gitea.io/gitea/modules/migration"
@@ -506,30 +508,8 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co
 			return nil, false, fmt.Errorf("error while listing comments: %v %w", g.repoID, err)
 		}
 		for _, comment := range comments {
-			// Flatten comment threads
-			if !comment.IndividualNote {
-				for _, note := range comment.Notes {
-					allComments = append(allComments, &base.Comment{
-						IssueIndex:  commentable.GetLocalIndex(),
-						Index:       int64(note.ID),
-						PosterID:    int64(note.Author.ID),
-						PosterName:  note.Author.Username,
-						PosterEmail: note.Author.Email,
-						Content:     note.Body,
-						Created:     *note.CreatedAt,
-					})
-				}
-			} else {
-				c := comment.Notes[0]
-				allComments = append(allComments, &base.Comment{
-					IssueIndex:  commentable.GetLocalIndex(),
-					Index:       int64(c.ID),
-					PosterID:    int64(c.Author.ID),
-					PosterName:  c.Author.Username,
-					PosterEmail: c.Author.Email,
-					Content:     c.Body,
-					Created:     *c.CreatedAt,
-				})
+			for _, note := range comment.Notes {
+				allComments = append(allComments, g.convertNoteToComment(commentable.GetLocalIndex(), note))
 			}
 		}
 		if resp.NextPage == 0 {
@@ -537,9 +517,93 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co
 		}
 		page = resp.NextPage
 	}
+
+	page = 1
+	for {
+		var stateEvents []*gitlab.StateEvent
+		var resp *gitlab.Response
+		var err error
+		if context.IsMergeRequest {
+			stateEvents, resp, err = g.client.ResourceStateEvents.ListMergeStateEvents(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListStateEventsOptions{
+				ListOptions: gitlab.ListOptions{
+					Page:    page,
+					PerPage: g.maxPerPage,
+				},
+			}, nil, gitlab.WithContext(g.ctx))
+		} else {
+			stateEvents, resp, err = g.client.ResourceStateEvents.ListIssueStateEvents(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListStateEventsOptions{
+				ListOptions: gitlab.ListOptions{
+					Page:    page,
+					PerPage: g.maxPerPage,
+				},
+			}, nil, gitlab.WithContext(g.ctx))
+		}
+		if err != nil {
+			return nil, false, fmt.Errorf("error while listing state events: %v %w", g.repoID, err)
+		}
+
+		for _, stateEvent := range stateEvents {
+			comment := &base.Comment{
+				IssueIndex: commentable.GetLocalIndex(),
+				Index:      int64(stateEvent.ID),
+				PosterID:   int64(stateEvent.User.ID),
+				PosterName: stateEvent.User.Username,
+				Content:    "",
+				Created:    *stateEvent.CreatedAt,
+			}
+			switch stateEvent.State {
+			case gitlab.ClosedEventType:
+				comment.CommentType = issues_model.CommentTypeClose.String()
+			case gitlab.MergedEventType:
+				comment.CommentType = issues_model.CommentTypeMergePull.String()
+			case gitlab.ReopenedEventType:
+				comment.CommentType = issues_model.CommentTypeReopen.String()
+			default:
+				// Ignore other event types
+				continue
+			}
+			allComments = append(allComments, comment)
+		}
+
+		if resp.NextPage == 0 {
+			break
+		}
+		page = resp.NextPage
+	}
+
 	return allComments, true, nil
 }
 
+var targetBranchChangeRegexp = regexp.MustCompile("^changed target branch from `(.*?)` to `(.*?)`$")
+
+func (g *GitlabDownloader) convertNoteToComment(localIndex int64, note *gitlab.Note) *base.Comment {
+	comment := &base.Comment{
+		IssueIndex:  localIndex,
+		Index:       int64(note.ID),
+		PosterID:    int64(note.Author.ID),
+		PosterName:  note.Author.Username,
+		PosterEmail: note.Author.Email,
+		Content:     note.Body,
+		Created:     *note.CreatedAt,
+		Meta:        map[string]any{},
+	}
+
+	// Try to find the underlying event of system notes.
+	if note.System {
+		if match := targetBranchChangeRegexp.FindStringSubmatch(note.Body); match != nil {
+			comment.CommentType = issues_model.CommentTypeChangeTargetBranch.String()
+			comment.Meta["OldRef"] = match[1]
+			comment.Meta["NewRef"] = match[2]
+		} else if strings.HasPrefix(note.Body, "enabled an automatic merge") {
+			comment.CommentType = issues_model.CommentTypePRScheduledToAutoMerge.String()
+		} else if note.Body == "canceled the automatic merge" {
+			comment.CommentType = issues_model.CommentTypePRUnScheduledToAutoMerge.String()
+		}
+	}
+
+	return comment
+}
+
 // GetPullRequests returns pull requests according page and perPage
 func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
 	if perPage > g.maxPerPage {
diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go
index 1e0aa2b025..0b9eeaed54 100644
--- a/services/migrations/gitlab_test.go
+++ b/services/migrations/gitlab_test.go
@@ -517,6 +517,88 @@ func TestAwardsToReactions(t *testing.T) {
 	}, reactions)
 }
 
+func TestNoteToComment(t *testing.T) {
+	downloader := &GitlabDownloader{}
+
+	now := time.Now()
+	makeTestNote := func(id int, body string, system bool) gitlab.Note {
+		return gitlab.Note{
+			ID: id,
+			Author: struct {
+				ID        int    `json:"id"`
+				Username  string `json:"username"`
+				Email     string `json:"email"`
+				Name      string `json:"name"`
+				State     string `json:"state"`
+				AvatarURL string `json:"avatar_url"`
+				WebURL    string `json:"web_url"`
+			}{
+				ID:       72,
+				Email:    "test@example.com",
+				Username: "test",
+			},
+			Body:      body,
+			CreatedAt: &now,
+			System:    system,
+		}
+	}
+	notes := []gitlab.Note{
+		makeTestNote(1, "This is a regular comment", false),
+		makeTestNote(2, "enabled an automatic merge for abcd1234", true),
+		makeTestNote(3, "changed target branch from `master` to `main`", true),
+		makeTestNote(4, "canceled the automatic merge", true),
+	}
+	comments := []base.Comment{{
+		IssueIndex:  17,
+		Index:       1,
+		PosterID:    72,
+		PosterName:  "test",
+		PosterEmail: "test@example.com",
+		CommentType: "",
+		Content:     "This is a regular comment",
+		Created:     now,
+		Meta:        map[string]any{},
+	}, {
+		IssueIndex:  17,
+		Index:       2,
+		PosterID:    72,
+		PosterName:  "test",
+		PosterEmail: "test@example.com",
+		CommentType: "pull_scheduled_merge",
+		Content:     "enabled an automatic merge for abcd1234",
+		Created:     now,
+		Meta:        map[string]any{},
+	}, {
+		IssueIndex:  17,
+		Index:       3,
+		PosterID:    72,
+		PosterName:  "test",
+		PosterEmail: "test@example.com",
+		CommentType: "change_target_branch",
+		Content:     "changed target branch from `master` to `main`",
+		Created:     now,
+		Meta: map[string]any{
+			"OldRef": "master",
+			"NewRef": "main",
+		},
+	}, {
+		IssueIndex:  17,
+		Index:       4,
+		PosterID:    72,
+		PosterName:  "test",
+		PosterEmail: "test@example.com",
+		CommentType: "pull_cancel_scheduled_merge",
+		Content:     "canceled the automatic merge",
+		Created:     now,
+		Meta:        map[string]any{},
+	}}
+
+	for i, note := range notes {
+		actualComment := *downloader.convertNoteToComment(17, &note)
+		assert.EqualValues(t, actualComment, comments[i])
+	}
+}
+
 func TestGitlabIIDResolver(t *testing.T) {
 	r := gitlabIIDResolver{}
 	r.recordIssueIID(1)
diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go
index 0b83f3b4a3..5bb3056161 100644
--- a/services/migrations/migrate.go
+++ b/services/migrations/migrate.go
@@ -250,14 +250,13 @@ func migrateRepository(ctx context.Context, doer *user_model.User, downloader ba
 			}
 			log.Warn("migrating milestones is not supported, ignored")
 		}
-
 		msBatchSize := uploader.MaxBatchInsertSize("milestone")
 		for len(milestones) > 0 {
 			if len(milestones) < msBatchSize {
 				msBatchSize = len(milestones)
 			}
 
-			if err := uploader.CreateMilestones(milestones...); err != nil {
+			if err := uploader.CreateMilestones(milestones[:msBatchSize]...); err != nil {
 				return err
 			}
 			milestones = milestones[msBatchSize:]
diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go
index 33664562fe..21d5f08205 100644
--- a/services/mirror/mirror_pull.go
+++ b/services/mirror/mirror_pull.go
@@ -449,19 +449,17 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
 		return false
 	}
 
-	var gitRepo *git.Repository
-	if len(results) == 0 {
-		log.Trace("SyncMirrors [repo: %-v]: no branches updated", m.Repo)
-	} else {
-		log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results))
-		gitRepo, err = gitrepo.OpenRepository(ctx, m.Repo)
-		if err != nil {
-			log.Error("SyncMirrors [repo: %-v]: unable to OpenRepository: %v", m.Repo, err)
-			return false
-		}
-		defer gitRepo.Close()
+	gitRepo, err := gitrepo.OpenRepository(ctx, m.Repo)
+	if err != nil {
+		log.Error("SyncMirrors [repo: %-v]: unable to OpenRepository: %v", m.Repo, err)
+		return false
+	}
+	defer gitRepo.Close()
 
+	log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results))
+	if len(results) > 0 {
 		if ok := checkAndUpdateEmptyRepository(ctx, m, gitRepo, results); !ok {
+			log.Error("SyncMirrors [repo: %-v]: checkAndUpdateEmptyRepository: %v", m.Repo, err)
 			return false
 		}
 	}
@@ -479,10 +477,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
 				log.Error("SyncMirrors [repo: %-v]: unable to GetRefCommitID [ref_name: %s]: %v", m.Repo, result.refName, err)
 				continue
 			}
-			objectFormat, err := gitrepo.GetObjectFormatOfRepo(ctx, m.Repo)
-			if err != nil {
-				log.Error("SyncMirrors [repo: %-v]: unable to GetHashTypeOfRepo: %v", m.Repo, err)
-			}
+			objectFormat := git.ObjectFormatFromName(m.Repo.ObjectFormatName)
 			notify_service.SyncPushCommits(ctx, m.Repo.MustOwner(ctx), m.Repo, &repo_module.PushUpdateOptions{
 				RefFullName: result.refName,
 				OldCommitID: objectFormat.EmptyObjectID().String(),
@@ -537,16 +532,24 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
 	}
 	log.Trace("SyncMirrors [repo: %-v]: done notifying updated branches/tags - now updating last commit time", m.Repo)
 
-	// Get latest commit date and update to current repository updated time
-	commitDate, err := git.GetLatestCommitTime(ctx, m.Repo.RepoPath())
+	isEmpty, err := gitRepo.IsEmpty()
 	if err != nil {
-		log.Error("SyncMirrors [repo: %-v]: unable to GetLatestCommitDate: %v", m.Repo, err)
+		log.Error("SyncMirrors [repo: %-v]: unable to check empty git repo: %v", m.Repo, err)
 		return false
 	}
+	if !isEmpty {
+		// Get latest commit date and update to current repository updated time
+		commitDate, err := git.GetLatestCommitTime(ctx, m.Repo.RepoPath())
+		if err != nil {
+			log.Error("SyncMirrors [repo: %-v]: unable to GetLatestCommitDate: %v", m.Repo, err)
+			return false
+		}
+
+		if err = repo_model.UpdateRepositoryUpdatedTime(ctx, m.RepoID, commitDate); err != nil {
+			log.Error("SyncMirrors [repo: %-v]: unable to update repository 'updated_unix': %v", m.Repo, err)
+			return false
+		}
 
-	if err = repo_model.UpdateRepositoryUpdatedTime(ctx, m.RepoID, commitDate); err != nil {
-		log.Error("SyncMirrors [repo: %-v]: unable to update repository 'updated_unix': %v", m.Repo, err)
-		return false
 	}
 
 	log.Trace("SyncMirrors [repo: %-v]: Successfully updated", m.Repo)
@@ -593,7 +596,7 @@ func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, gi
 			m.Repo.DefaultBranch = firstName
 		}
 		// Update the git repository default branch
-		if err := gitRepo.SetDefaultBranch(m.Repo.DefaultBranch); err != nil {
+		if err := gitrepo.SetDefaultBranch(ctx, m.Repo, m.Repo.DefaultBranch); err != nil {
 			if !git.IsErrUnsupportedVersion(err) {
 				log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err)
 				desc := fmt.Sprintf("Failed to update default branch of underlying git repository '%s': %v", m.Repo.RepoPath(), err)
diff --git a/services/notify/notify.go b/services/notify/notify.go
index 16fbb6325d..0c8262ef7a 100644
--- a/services/notify/notify.go
+++ b/services/notify/notify.go
@@ -91,7 +91,7 @@ func AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues
 // NewPullRequest notifies new pull request to notifiers
 func NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User) {
 	if err := pr.LoadIssue(ctx); err != nil {
-		log.Error("%v", err)
+		log.Error("LoadIssue failed: %v", err)
 		return
 	}
 	if err := pr.Issue.LoadPoster(ctx); err != nil {
@@ -112,7 +112,7 @@ func PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *iss
 // PullRequestReview notifies new pull request review
 func PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
 	if err := review.LoadReviewer(ctx); err != nil {
-		log.Error("%v", err)
+		log.Error("LoadReviewer failed: %v", err)
 		return
 	}
 	for _, notifier := range notifiers {
diff --git a/services/packages/alpine/repository.go b/services/packages/alpine/repository.go
index 104548b421..664ab34559 100644
--- a/services/packages/alpine/repository.go
+++ b/services/packages/alpine/repository.go
@@ -23,6 +23,7 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	alpine_model "code.gitea.io/gitea/models/packages/alpine"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/json"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
@@ -30,7 +31,10 @@ import (
 	packages_service "code.gitea.io/gitea/services/packages"
 )
 
-const IndexFilename = "APKINDEX.tar.gz"
+const (
+	IndexFilename        = "APKINDEX"
+	IndexArchiveFilename = IndexFilename + ".tar.gz"
+)
 
 // GetOrCreateRepositoryVersion gets or creates the internal repository package
 // The Alpine registry needs multiple index files which are stored in this package.
@@ -120,7 +124,22 @@ func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, branch, re
 		return err
 	}
 
-	return buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture)
+	architectures := container.SetOf(architecture)
+	if architecture == alpine_module.NoArch {
+		// Update all other architectures too when updating the noarch index
+		additionalArchitectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository)
+		if err != nil {
+			return err
+		}
+		architectures.AddMultiple(additionalArchitectures...)
+	}
+
+	for architecture := range architectures {
+		if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil {
+			return err
+		}
+	}
+	return nil
 }
 
 type packageData struct {
@@ -133,8 +152,7 @@ type packageData struct {
 
 type packageCache = map[*packages_model.PackageFile]*packageData
 
-// https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format
-func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error {
+func searchPackageFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) ([]*packages_model.PackageFile, error) {
 	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
 		OwnerID:     ownerID,
 		PackageType: packages_model.TypeAlpine,
@@ -145,13 +163,30 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
 			alpine_module.PropertyArchitecture: architecture,
 		},
 	})
+	if err != nil {
+		return nil, err
+	}
+	return pfs, nil
+}
+
+// https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format
+func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error {
+	pfs, err := searchPackageFiles(ctx, ownerID, branch, repository, architecture)
 	if err != nil {
 		return err
 	}
+	if architecture != alpine_module.NoArch {
+		// Add all noarch packages too
+		noarchFiles, err := searchPackageFiles(ctx, ownerID, branch, repository, alpine_module.NoArch)
+		if err != nil {
+			return err
+		}
+		pfs = append(pfs, noarchFiles...)
+	}
 
 	// Delete the package indices if there are no packages
 	if len(pfs) == 0 {
-		pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture))
+		pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexArchiveFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture))
 		if err != nil && !errors.Is(err, util.ErrNotExist) {
 			return err
 		} else if pf == nil {
@@ -206,7 +241,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
 		fmt.Fprintf(&buf, "C:%s\n", pd.FileMetadata.Checksum)
 		fmt.Fprintf(&buf, "P:%s\n", pd.Package.Name)
 		fmt.Fprintf(&buf, "V:%s\n", pd.Version.Version)
-		fmt.Fprintf(&buf, "A:%s\n", pd.FileMetadata.Architecture)
+		fmt.Fprintf(&buf, "A:%s\n", architecture)
 		if pd.VersionMetadata.Description != "" {
 			fmt.Fprintf(&buf, "T:%s\n", pd.VersionMetadata.Description)
 		}
@@ -244,7 +279,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
 
 	h := sha1.New()
 
-	if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), "APKINDEX", buf.Bytes(), true); err != nil {
+	if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), IndexFilename, buf.Bytes(), true); err != nil {
 		return err
 	}
 
@@ -299,13 +334,18 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
 		repoVersion,
 		&packages_service.PackageFileCreationInfo{
 			PackageFileInfo: packages_service.PackageFileInfo{
-				Filename:     IndexFilename,
+				Filename:     IndexArchiveFilename,
 				CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
 			},
 			Creator:           user_model.NewGhostUser(),
 			Data:              signedIndexContent,
 			IsLead:            false,
 			OverwriteExisting: true,
+			Properties: map[string]string{
+				alpine_module.PropertyBranch:       branch,
+				alpine_module.PropertyRepository:   repository,
+				alpine_module.PropertyArchitecture: architecture,
+			},
 		},
 	)
 	return err
diff --git a/services/packages/auth.go b/services/packages/auth.go
index 2f78b26f50..8263c28bed 100644
--- a/services/packages/auth.go
+++ b/services/packages/auth.go
@@ -33,7 +33,7 @@ func CreateAuthorizationToken(u *user_model.User) (string, error) {
 	}
 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
 
-	tokenString, err := token.SignedString([]byte(setting.SecretKey))
+	tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
 	if err != nil {
 		return "", err
 	}
@@ -57,7 +57,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
 		if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
 			return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
 		}
-		return []byte(setting.SecretKey), nil
+		return setting.GetGeneralTokenSigningSecret(), nil
 	})
 	if err != nil {
 		return 0, err
diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go
index 0ff8077bc9..5d5120c6a0 100644
--- a/services/packages/cleanup/cleanup.go
+++ b/services/packages/cleanup/cleanup.go
@@ -12,8 +12,8 @@ import (
 	packages_model "code.gitea.io/gitea/models/packages"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
-	"code.gitea.io/gitea/modules/util"
 	packages_service "code.gitea.io/gitea/services/packages"
 	alpine_service "code.gitea.io/gitea/services/packages/alpine"
 	cargo_service "code.gitea.io/gitea/services/packages/cargo"
@@ -60,7 +60,7 @@ func ExecuteCleanupRules(outerCtx context.Context) error {
 		for _, p := range packages {
 			pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 				PackageID:  p.ID,
-				IsInternal: util.OptionalBoolFalse,
+				IsInternal: optional.Some(false),
 				Sort:       packages_model.SortCreatedDesc,
 				Paginator:  db.NewAbsoluteListOptions(pcr.KeepCount, 200),
 			})
diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go
index dd3f158dbf..3f5f43bbc0 100644
--- a/services/packages/container/cleanup.go
+++ b/services/packages/container/cleanup.go
@@ -9,8 +9,8 @@ import (
 
 	packages_model "code.gitea.io/gitea/models/packages"
 	container_model "code.gitea.io/gitea/models/packages/container"
+	"code.gitea.io/gitea/modules/optional"
 	container_module "code.gitea.io/gitea/modules/packages/container"
-	"code.gitea.io/gitea/modules/util"
 	packages_service "code.gitea.io/gitea/services/packages"
 
 	digest "github.com/opencontainers/go-digest"
@@ -59,8 +59,8 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e
 			ExactMatch: true,
 			Value:      container_model.UploadVersion,
 		},
-		IsInternal: util.OptionalBoolTrue,
-		HasFiles:   util.OptionalBoolFalse,
+		IsInternal: optional.Some(true),
+		HasFiles:   optional.Some(false),
 	})
 	if err != nil {
 		return err
diff --git a/services/packages/debian/repository.go b/services/packages/debian/repository.go
index 86c54e40c8..611faa6ade 100644
--- a/services/packages/debian/repository.go
+++ b/services/packages/debian/repository.go
@@ -342,7 +342,7 @@ func buildReleaseFiles(ctx context.Context, ownerID int64, repoVersion *packages
 	fmt.Fprintf(w, "Components: %s\n", strings.Join(components, " "))
 	fmt.Fprintf(w, "Architectures: %s\n", strings.Join(architectures, " "))
 	fmt.Fprintf(w, "Date: %s\n", time.Now().UTC().Format(time.RFC1123))
-	fmt.Fprint(w, "Acquire-By-Hash: yes")
+	fmt.Fprint(w, "Acquire-By-Hash: yes\n")
 
 	pfds, err := packages_model.GetPackageFileDescriptors(ctx, pfs)
 	if err != nil {
diff --git a/services/packages/packages.go b/services/packages/packages.go
index 56d5cc04de..64b1ddd869 100644
--- a/services/packages/packages.go
+++ b/services/packages/packages.go
@@ -18,10 +18,10 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
-	"code.gitea.io/gitea/modules/util"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
 
@@ -330,7 +330,7 @@ func CheckCountQuotaExceeded(ctx context.Context, doer, owner *user_model.User)
 	if setting.Packages.LimitTotalOwnerCount > -1 {
 		totalCount, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
 			OwnerID:    owner.ID,
-			IsInternal: util.OptionalBoolFalse,
+			IsInternal: optional.Some(false),
 		})
 		if err != nil {
 			log.Error("CountVersions failed: %v", err)
@@ -640,7 +640,7 @@ func RemoveAllPackages(ctx context.Context, userID int64) (int, error) {
 				Page:     1,
 			},
 			OwnerID:    userID,
-			IsInternal: util.OptionalBoolNone,
+			IsInternal: optional.None[bool](),
 		})
 		if err != nil {
 			return count, fmt.Errorf("GetOwnedPackages[%d]: %w", userID, err)
diff --git a/services/pull/check.go b/services/pull/check.go
index dd6c3ed230..f4dd332b14 100644
--- a/services/pull/check.go
+++ b/services/pull/check.go
@@ -222,10 +222,7 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com
 	}
 	defer gitRepo.Close()
 
-	objectFormat, err := gitRepo.GetObjectFormat()
-	if err != nil {
-		return nil, fmt.Errorf("%-v GetObjectFormat: %w", pr.BaseRepo, err)
-	}
+	objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
 
 	// Get the commit from BaseBranch where the pull request got merged
 	mergeCommit, _, err := git.NewCommand(ctx, "rev-list", "--ancestry-path", "--merges", "--reverse").
diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index a602ddf106..c4b4709fe3 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -34,9 +34,9 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
 			}
 		}
 
-		for _, commitStatus := range commitStatuses {
+		for _, gp := range requiredContextsGlob {
 			var targetStatus structs.CommitStatusState
-			for _, gp := range requiredContextsGlob {
+			for _, commitStatus := range commitStatuses {
 				if gp.Match(commitStatus.Context) {
 					targetStatus = commitStatus.State
 					matchedCount++
@@ -44,13 +44,21 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
 				}
 			}
 
-			if targetStatus != "" && targetStatus.NoBetterThan(returnedStatus) {
+			// If required rule not match any action, then it is pending
+			if targetStatus == "" {
+				if structs.CommitStatusPending.NoBetterThan(returnedStatus) {
+					returnedStatus = structs.CommitStatusPending
+				}
+				break
+			}
+
+			if targetStatus.NoBetterThan(returnedStatus) {
 				returnedStatus = targetStatus
 			}
 		}
 	}
 
-	if matchedCount == 0 {
+	if matchedCount == 0 && returnedStatus == structs.CommitStatusSuccess {
 		status := git_model.CalcCommitStatus(commitStatuses)
 		if status != nil {
 			return status.State
@@ -143,7 +151,7 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR
 		return "", errors.Wrap(err, "LoadBaseRepo")
 	}
 
-	commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptions{ListAll: true})
+	commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll)
 	if err != nil {
 		return "", errors.Wrap(err, "GetLatestCommitStatus")
 	}
diff --git a/services/pull/commit_status_test.go b/services/pull/commit_status_test.go
new file mode 100644
index 0000000000..592acdd55c
--- /dev/null
+++ b/services/pull/commit_status_test.go
@@ -0,0 +1,65 @@
+// Copyright 2024 The Gitea Authors.
+// All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull
+
+import (
+	"testing"
+
+	git_model "code.gitea.io/gitea/models/git"
+	"code.gitea.io/gitea/modules/structs"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMergeRequiredContextsCommitStatus(t *testing.T) {
+	testCases := [][]*git_model.CommitStatus{
+		{
+			{Context: "Build 1", State: structs.CommitStatusSuccess},
+			{Context: "Build 2", State: structs.CommitStatusSuccess},
+			{Context: "Build 3", State: structs.CommitStatusSuccess},
+		},
+		{
+			{Context: "Build 1", State: structs.CommitStatusSuccess},
+			{Context: "Build 2", State: structs.CommitStatusSuccess},
+			{Context: "Build 2t", State: structs.CommitStatusPending},
+		},
+		{
+			{Context: "Build 1", State: structs.CommitStatusSuccess},
+			{Context: "Build 2", State: structs.CommitStatusSuccess},
+			{Context: "Build 2t", State: structs.CommitStatusFailure},
+		},
+		{
+			{Context: "Build 1", State: structs.CommitStatusSuccess},
+			{Context: "Build 2", State: structs.CommitStatusSuccess},
+			{Context: "Build 2t", State: structs.CommitStatusSuccess},
+		},
+		{
+			{Context: "Build 1", State: structs.CommitStatusSuccess},
+			{Context: "Build 2", State: structs.CommitStatusSuccess},
+			{Context: "Build 2t", State: structs.CommitStatusSuccess},
+		},
+	}
+	testCasesRequiredContexts := [][]string{
+		{"Build*"},
+		{"Build*", "Build 2t*"},
+		{"Build*", "Build 2t*"},
+		{"Build*", "Build 2t*", "Build 3*"},
+		{"Build*", "Build *", "Build 2t*", "Build 1*"},
+	}
+
+	testCasesExpected := []structs.CommitStatusState{
+		structs.CommitStatusSuccess,
+		structs.CommitStatusPending,
+		structs.CommitStatusFailure,
+		structs.CommitStatusPending,
+		structs.CommitStatusSuccess,
+	}
+
+	for i, commitStatuses := range testCases {
+		if MergeRequiredContextsCommitStatus(commitStatuses, testCasesRequiredContexts[i]) != testCasesExpected[i] {
+			assert.Fail(t, "Test case failed", "Test case %d failed", i+1)
+		}
+	}
+}
diff --git a/services/pull/merge.go b/services/pull/merge.go
index 63f0268beb..e37540a96f 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -267,6 +267,10 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use
 		if err := doMergeStyleSquash(mergeCtx, message); err != nil {
 			return "", err
 		}
+	case repo_model.MergeStyleFastForwardOnly:
+		if err := doMergeStyleFastForwardOnly(mergeCtx); err != nil {
+			return "", err
+		}
 	default:
 		return "", models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle}
 	}
@@ -377,6 +381,13 @@ func runMergeCommand(ctx *mergeContext, mergeStyle repo_model.MergeStyle, cmd *g
 				StdErr: ctx.errbuf.String(),
 				Err:    err,
 			}
+		} else if mergeStyle == repo_model.MergeStyleFastForwardOnly && strings.Contains(ctx.errbuf.String(), "Not possible to fast-forward, aborting") {
+			log.Debug("MergeDivergingFastForwardOnly %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
+			return models.ErrMergeDivergingFastForwardOnly{
+				StdOut: ctx.outbuf.String(),
+				StdErr: ctx.errbuf.String(),
+				Err:    err,
+			}
 		}
 		log.Error("git merge %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
 		return fmt.Errorf("git merge %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
@@ -486,7 +497,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use
 			return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: repo_model.MergeStyleManuallyMerged}
 		}
 
-		objectFormat, _ := baseGitRepo.GetObjectFormat()
+		objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
 		if len(commitID) != objectFormat.FullLength() {
 			return fmt.Errorf("Wrong commit ID")
 		}
diff --git a/services/pull/merge_ff_only.go b/services/pull/merge_ff_only.go
new file mode 100644
index 0000000000..f57c732104
--- /dev/null
+++ b/services/pull/merge_ff_only.go
@@ -0,0 +1,21 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull
+
+import (
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
+)
+
+// doMergeStyleFastForwardOnly merges the tracking into the current HEAD - which is assumed to be staging branch (equal to the pr.BaseBranch)
+func doMergeStyleFastForwardOnly(ctx *mergeContext) error {
+	cmd := git.NewCommand(ctx, "merge", "--ff-only").AddDynamicArguments(trackingBranch)
+	if err := runMergeCommand(ctx, repo_model.MergeStyleFastForwardOnly, cmd); err != nil {
+		log.Error("%-v Unable to merge tracking into base: %v", ctx.pr, err)
+		return err
+	}
+
+	return nil
+}
diff --git a/services/pull/merge_merge.go b/services/pull/merge_merge.go
index 0f7664297a..bf56c071db 100644
--- a/services/pull/merge_merge.go
+++ b/services/pull/merge_merge.go
@@ -9,7 +9,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 )
 
-// doMergeStyleMerge merges the tracking into the current HEAD - which is assumed to tbe staging branch (equal to the pr.BaseBranch)
+// doMergeStyleMerge merges the tracking branch into the current HEAD - which is assumed to be the staging branch (equal to the pr.BaseBranch)
 func doMergeStyleMerge(ctx *mergeContext, message string) error {
 	cmd := git.NewCommand(ctx, "merge", "--no-ff", "--no-commit").AddDynamicArguments(trackingBranch)
 	if err := runMergeCommand(ctx, repo_model.MergeStyleMerge, cmd); err != nil {
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 1182a75c89..80eaf67341 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -21,7 +21,6 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/graceful"
@@ -31,6 +30,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/sync"
 	"code.gitea.io/gitea/modules/util"
+	gitea_context "code.gitea.io/gitea/services/context"
 	issue_service "code.gitea.io/gitea/services/issue"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
@@ -40,6 +40,14 @@ var pullWorkingPool = sync.NewExclusivePool()
 
 // NewPullRequest creates new pull request with labels for repository.
 func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
+	if err := issue.LoadPoster(ctx); err != nil {
+		return err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
+		return user_model.ErrBlockedUser
+	}
+
 	prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
 	if err != nil {
 		if !git_model.IsErrBranchNotExist(err) {
@@ -69,6 +77,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
 	}
 	defer baseGitRepo.Close()
 
+	var reviewNotifers []*issue_service.ReviewRequestNotifier
 	if err := db.WithTx(ctx, func(ctx context.Context) error {
 		if err := issues_model.NewPullRequest(ctx, repo, issue, labelIDs, uuids, pr); err != nil {
 			return err
@@ -128,7 +137,8 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
 		}
 
 		if !pr.IsWorkInProgress(ctx) {
-			if err := issues_model.PullRequestCodeOwnersReview(ctx, issue, pr); err != nil {
+			reviewNotifers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr)
+			if err != nil {
 				return err
 			}
 		}
@@ -142,11 +152,12 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
 	}
 	baseGitRepo.Close() // close immediately to avoid notifications will open the repository again
 
+	issue_service.ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifers)
+
 	mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content)
 	if err != nil {
 		return err
 	}
-
 	notify_service.NewPullRequest(ctx, pr, mentions)
 	if len(issue.Labels) > 0 {
 		notify_service.IssueChangeLabels(ctx, issue.Poster, issue, issue.Labels, nil)
@@ -329,7 +340,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 			}
 			if err == nil {
 				for _, pr := range prs {
-					objectFormat, _ := gitrepo.GetObjectFormatOfRepo(ctx, pr.BaseRepo)
+					objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
 					if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() {
 						changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID)
 						if err != nil {
@@ -515,6 +526,25 @@ func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, pre
 	return nil
 }
 
+// UpdatePullsRefs update all the PRs head file pointers like /refs/pull/1/head so that it will be dependent by other operations
+func UpdatePullsRefs(ctx context.Context, repo *repo_model.Repository, update *repo_module.PushUpdateOptions) {
+	branch := update.RefFullName.BranchName()
+	// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
+	prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repo.ID, branch)
+	if err != nil {
+		log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repo.ID, branch, err)
+	} else {
+		for _, pr := range prs {
+			log.Trace("Updating PR[%d]: composing new test task", pr.ID)
+			if pr.Flow == issues_model.PullRequestFlowGithub {
+				if err := PushToBaseRepo(ctx, pr); err != nil {
+					log.Error("PushToBaseRepo: %v", err)
+				}
+			}
+		}
+	}
+}
+
 // UpdateRef update refs/pull/id/head directly for agit flow pull request
 func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) {
 	log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitRefName())
@@ -872,7 +902,7 @@ func getAllCommitStatus(ctx context.Context, gitRepo *git.Repository, pr *issues
 		return nil, nil, shaErr
 	}
 
-	statuses, _, err = git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptions{ListAll: true})
+	statuses, _, err = git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll)
 	lastStatus = git_model.CalcCommitStatus(statuses)
 	return statuses, lastStatus, err
 }
@@ -959,12 +989,12 @@ func GetPullCommits(ctx *gitea_context.Context, issue *issues_model.Issue) ([]Co
 	for _, commit := range prInfo.Commits {
 		var committerOrAuthorName string
 		var commitTime time.Time
-		if commit.Committer != nil {
-			committerOrAuthorName = commit.Committer.Name
-			commitTime = commit.Committer.When
-		} else {
+		if commit.Author != nil {
 			committerOrAuthorName = commit.Author.Name
 			commitTime = commit.Author.When
+		} else {
+			committerOrAuthorName = commit.Committer.Name
+			commitTime = commit.Committer.When
 		}
 
 		commits = append(commits, CommitInfo{
diff --git a/services/pull/review.go b/services/pull/review.go
index d4ea975612..5bf1991d13 100644
--- a/services/pull/review.go
+++ b/services/pull/review.go
@@ -18,6 +18,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	notify_service "code.gitea.io/gitea/services/notify"
@@ -25,6 +26,23 @@ import (
 
 var notEnoughLines = regexp.MustCompile(`fatal: file .* has only \d+ lines?`)
 
+// ErrDismissRequestOnClosedPR represents an error when an user tries to dismiss a review associated to a closed or merged PR.
+type ErrDismissRequestOnClosedPR struct{}
+
+// IsErrDismissRequestOnClosedPR checks if an error is an ErrDismissRequestOnClosedPR.
+func IsErrDismissRequestOnClosedPR(err error) bool {
+	_, ok := err.(ErrDismissRequestOnClosedPR)
+	return ok
+}
+
+func (err ErrDismissRequestOnClosedPR) Error() string {
+	return "can't dismiss a review associated to a closed or merged PR"
+}
+
+func (err ErrDismissRequestOnClosedPR) Unwrap() error {
+	return util.ErrPermissionDenied
+}
+
 // checkInvalidation checks if the line of code comment got changed by another commit.
 // If the line got changed the comment is going to be invalidated.
 func checkInvalidation(ctx context.Context, c *issues_model.Comment, doer *user_model.User, repo *git.Repository, branch string) error {
@@ -52,11 +70,9 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis
 	issueIDs := prs.GetIssueIDs()
 
 	codeComments, err := db.Find[issues_model.Comment](ctx, issues_model.FindCommentsOptions{
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
+		ListOptions: db.ListOptionsAll,
 		Type:        issues_model.CommentTypeCode,
-		Invalidated: util.OptionalBoolFalse,
+		Invalidated: optional.Some(false),
 		IssueIDs:    issueIDs,
 	})
 	if err != nil {
@@ -71,7 +87,7 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis
 }
 
 // CreateCodeComment creates a comment on the code line
-func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string) (*issues_model.Comment, error) {
+func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string, attachments []string) (*issues_model.Comment, error) {
 	var (
 		existsReview bool
 		err          error
@@ -104,6 +120,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
 			treePath,
 			line,
 			replyReviewID,
+			attachments,
 		)
 		if err != nil {
 			return nil, err
@@ -144,6 +161,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
 		treePath,
 		line,
 		review.ID,
+		attachments,
 	)
 	if err != nil {
 		return nil, err
@@ -162,7 +180,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
 }
 
 // createCodeComment creates a plain code comment at the specified line / path
-func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64) (*issues_model.Comment, error) {
+func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64, attachments []string) (*issues_model.Comment, error) {
 	var commitID, patch string
 	if err := issue.LoadPullRequest(ctx); err != nil {
 		return nil, fmt.Errorf("LoadPullRequest: %w", err)
@@ -260,16 +278,17 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
 		ReviewID:    reviewID,
 		Patch:       patch,
 		Invalidated: invalidated,
+		Attachments: attachments,
 	})
 }
 
 // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
 func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, reviewType issues_model.ReviewType, content, commitID string, attachmentUUIDs []string) (*issues_model.Review, *issues_model.Comment, error) {
-	pr, err := issue.GetPullRequest(ctx)
-	if err != nil {
+	if err := issue.LoadPullRequest(ctx); err != nil {
 		return nil, nil, err
 	}
 
+	pr := issue.PullRequest
 	var stale bool
 	if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject {
 		stale = false
@@ -319,12 +338,10 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
 // DismissApprovalReviews dismiss all approval reviews because of new commits
 func DismissApprovalReviews(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest) error {
 	reviews, err := issues_model.FindReviews(ctx, issues_model.FindReviewOptions{
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
-		IssueID:   pull.IssueID,
-		Type:      issues_model.ReviewTypeApprove,
-		Dismissed: util.OptionalBoolFalse,
+		ListOptions: db.ListOptionsAll,
+		IssueID:     pull.IssueID,
+		Type:        issues_model.ReviewTypeApprove,
+		Dismissed:   optional.Some(false),
 	})
 	if err != nil {
 		return err
@@ -383,6 +400,21 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string,
 		return nil, fmt.Errorf("reviews's repository is not the same as the one we expect")
 	}
 
+	issue := review.Issue
+
+	if issue.IsClosed {
+		return nil, ErrDismissRequestOnClosedPR{}
+	}
+
+	if issue.IsPull {
+		if err := issue.LoadPullRequest(ctx); err != nil {
+			return nil, err
+		}
+		if issue.PullRequest.HasMerged {
+			return nil, ErrDismissRequestOnClosedPR{}
+		}
+	}
+
 	if err := issues_model.DismissReview(ctx, review, isDismiss); err != nil {
 		return nil, err
 	}
@@ -391,7 +423,7 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string,
 		reviews, err := issues_model.FindReviews(ctx, issues_model.FindReviewOptions{
 			IssueID:    review.IssueID,
 			ReviewerID: review.ReviewerID,
-			Dismissed:  util.OptionalBoolFalse,
+			Dismissed:  optional.Some(false),
 		})
 		if err != nil {
 			return nil, err
diff --git a/services/pull/review_test.go b/services/pull/review_test.go
new file mode 100644
index 0000000000..3bce1e523d
--- /dev/null
+++ b/services/pull/review_test.go
@@ -0,0 +1,48 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull_test
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	pull_service "code.gitea.io/gitea/services/pull"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDismissReview(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{})
+	assert.NoError(t, pull.LoadIssue(db.DefaultContext))
+	issue := pull.Issue
+	assert.NoError(t, issue.LoadRepo(db.DefaultContext))
+	reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
+		Issue:    issue,
+		Reviewer: reviewer,
+		Type:     issues_model.ReviewTypeReject,
+	})
+
+	assert.NoError(t, err)
+	issue.IsClosed = true
+	pull.HasMerged = false
+	assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "is_closed"))
+	assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
+	_, err = pull_service.DismissReview(db.DefaultContext, review.ID, issue.RepoID, "", &user_model.User{}, false, false)
+	assert.Error(t, err)
+	assert.True(t, pull_service.IsErrDismissRequestOnClosedPR(err))
+
+	pull.HasMerged = true
+	pull.Issue.IsClosed = false
+	assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "is_closed"))
+	assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
+	_, err = pull_service.DismissReview(db.DefaultContext, review.ID, issue.RepoID, "", &user_model.User{}, false, false)
+	assert.Error(t, err)
+	assert.True(t, pull_service.IsErrDismissRequestOnClosedPR(err))
+}
diff --git a/services/release/release.go b/services/release/release.go
index e3b18d1f2b..ba5fd1dd98 100644
--- a/services/release/release.go
+++ b/services/release/release.go
@@ -88,7 +88,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel
 			created = true
 			rel.LowerTagName = strings.ToLower(rel.TagName)
 
-			objectFormat, _ := gitRepo.GetObjectFormat()
+			objectFormat := git.ObjectFormatFromName(rel.Repo.ObjectFormatName)
 			commits := repository.NewPushCommits()
 			commits.HeadCommit = repository.CommitToPushCommit(commit)
 			commits.CompareURL = rel.Repo.ComposeCompareURL(objectFormat.EmptyObjectID().String(), commit.ID.String())
@@ -326,10 +326,7 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
 		}
 
 		refName := git.RefNameFromTag(rel.TagName)
-		objectFormat, err := gitrepo.GetObjectFormatOfRepo(ctx, repo)
-		if err != nil {
-			return err
-		}
+		objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
 		notify_service.PushCommits(
 			ctx, doer, repo,
 			&repository.PushUpdateOptions{
diff --git a/services/repository/adopt.go b/services/repository/adopt.go
index 29517114a2..d57d6ea6d4 100644
--- a/services/repository/adopt.go
+++ b/services/repository/adopt.go
@@ -19,6 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
@@ -126,35 +127,26 @@ func adoptRepository(ctx context.Context, u *user_model.User, repo *repo_model.R
 
 	repo.IsEmpty = false
 
-	// Don't bother looking this repo in the context it won't be there
-	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
-	if err != nil {
-		return fmt.Errorf("openRepository: %w", err)
-	}
-	defer gitRepo.Close()
-
 	if len(defaultBranch) > 0 {
 		repo.DefaultBranch = defaultBranch
 
-		if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+		if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
 			return fmt.Errorf("setDefaultBranch: %w", err)
 		}
 	} else {
-		repo.DefaultBranch, err = gitRepo.GetDefaultBranch()
+		repo.DefaultBranch, err = gitrepo.GetDefaultBranch(ctx, repo)
 		if err != nil {
 			repo.DefaultBranch = setting.Repository.DefaultBranch
-			if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+			if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
 				return fmt.Errorf("setDefaultBranch: %w", err)
 			}
 		}
 	}
 
 	branches, _ := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
-		RepoID: repo.ID,
-		ListOptions: db.ListOptions{
-			ListAll: true,
-		},
-		IsDeletedBranch: util.OptionalBoolFalse,
+		RepoID:          repo.ID,
+		ListOptions:     db.ListOptionsAll,
+		IsDeletedBranch: optional.Some(false),
 	})
 
 	found := false
@@ -187,7 +179,7 @@ func adoptRepository(ctx context.Context, u *user_model.User, repo *repo_model.R
 			repo.DefaultBranch = setting.Repository.DefaultBranch
 		}
 
-		if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+		if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
 			return fmt.Errorf("setDefaultBranch: %w", err)
 		}
 	}
@@ -196,6 +188,13 @@ func adoptRepository(ctx context.Context, u *user_model.User, repo *repo_model.R
 		return fmt.Errorf("updateRepository: %w", err)
 	}
 
+	// Don't bother looking this repo in the context it won't be there
+	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
+	if err != nil {
+		return fmt.Errorf("openRepository: %w", err)
+	}
+	defer gitRepo.Close()
+
 	if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
 		return fmt.Errorf("SyncReleasesWithTags: %w", err)
 	}
diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go
index 5deec259da..ec6e9dfac3 100644
--- a/services/repository/archiver/archiver_test.go
+++ b/services/repository/archiver/archiver_test.go
@@ -10,7 +10,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
+	"code.gitea.io/gitea/services/contexttest"
 
 	_ "code.gitea.io/gitea/models/actions"
 
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 9ad8689ea3..229ac54f30 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -16,16 +16,19 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/cache"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/graceful"
+	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/queue"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 	notify_service "code.gitea.io/gitea/services/notify"
+	files_service "code.gitea.io/gitea/services/repository/files"
 
 	"xorm.io/builder"
 )
@@ -52,7 +55,7 @@ type Branch struct {
 }
 
 // LoadBranches loads branches from the repository limited by page & pageSize.
-func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch util.OptionalBool, keyword string, page, pageSize int) (*Branch, []*Branch, int64, error) {
+func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch optional.Option[bool], keyword string, page, pageSize int) (*Branch, []*Branch, int64, error) {
 	defaultDBBranch, err := git_model.GetBranch(ctx, repo.ID, repo.DefaultBranch)
 	if err != nil {
 		return nil, nil, 0, err
@@ -98,7 +101,6 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git
 		if err != nil {
 			return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err)
 		}
-
 		branches = append(branches, branch)
 	}
 
@@ -108,10 +110,44 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git
 	if err != nil {
 		return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err)
 	}
-
 	return defaultBranch, branches, totalNumOfBranches, nil
 }
 
+func getDivergenceCacheKey(repoID int64, branchName string) string {
+	return fmt.Sprintf("%d-%s", repoID, branchName)
+}
+
+// getDivergenceFromCache gets the divergence from cache
+func getDivergenceFromCache(repoID int64, branchName string) (*git.DivergeObject, bool) {
+	data := cache.GetCache().Get(getDivergenceCacheKey(repoID, branchName))
+	res := git.DivergeObject{
+		Ahead:  -1,
+		Behind: -1,
+	}
+	s, ok := data.([]byte)
+	if !ok || len(s) == 0 {
+		return &res, false
+	}
+
+	if err := json.Unmarshal(s, &res); err != nil {
+		log.Error("json.UnMarshal failed: %v", err)
+		return &res, false
+	}
+	return &res, true
+}
+
+func putDivergenceFromCache(repoID int64, branchName string, divergence *git.DivergeObject) error {
+	bs, err := json.Marshal(divergence)
+	if err != nil {
+		return err
+	}
+	return cache.GetCache().Put(getDivergenceCacheKey(repoID, branchName), bs, 30*24*60*60)
+}
+
+func DelDivergenceFromCache(repoID int64, branchName string) error {
+	return cache.GetCache().Delete(getDivergenceCacheKey(repoID, branchName))
+}
+
 func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *git_model.Branch, protectedBranches *git_model.ProtectedBranchRules,
 	repoIDToRepo map[int64]*repo_model.Repository,
 	repoIDToGitRepo map[int64]*git.Repository,
@@ -122,20 +158,30 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g
 	p := protectedBranches.GetFirstMatched(branchName)
 	isProtected := p != nil
 
-	divergence := &git.DivergeObject{
-		Ahead:  -1,
-		Behind: -1,
-	}
+	var divergence *git.DivergeObject
 
 	// it's not default branch
 	if repo.DefaultBranch != dbBranch.Name && !dbBranch.IsDeleted {
-		var err error
-		divergence, err = gitrepo.CountDivergingCommits(ctx, repo, repo.DefaultBranch, git.BranchPrefix+branchName)
-		if err != nil {
-			log.Error("CountDivergingCommits: %v", err)
+		var cached bool
+		divergence, cached = getDivergenceFromCache(repo.ID, dbBranch.Name)
+		if !cached {
+			var err error
+			divergence, err = files_service.CountDivergingCommits(ctx, repo, git.BranchPrefix+branchName)
+			if err != nil {
+				log.Error("CountDivergingCommits: %v", err)
+			} else {
+				if err = putDivergenceFromCache(repo.ID, dbBranch.Name, divergence); err != nil {
+					log.Error("putDivergenceFromCache: %v", err)
+				}
+			}
 		}
 	}
 
+	if divergence == nil {
+		// tolerate the error that we cannot get divergence
+		divergence = &git.DivergeObject{Ahead: -1, Behind: -1}
+	}
+
 	pr, err := issues_model.GetLatestPullRequestByHeadInfo(ctx, repo.ID, branchName)
 	if err != nil {
 		return nil, fmt.Errorf("GetLatestPullRequestByHeadInfo: %v", err)
@@ -220,44 +266,91 @@ func checkBranchName(ctx context.Context, repo *repo_model.Repository, name stri
 	return err
 }
 
-// syncBranchToDB sync the branch information in the database. It will try to update the branch first,
-// if updated success with affect records > 0, then all are done. Because that means the branch has been in the database.
-// If no record is affected, that means the branch does not exist in database. So there are two possibilities.
-// One is this is a new branch, then we just need to insert the record. Another is the branches haven't been synced,
-// then we need to sync all the branches into database.
-func syncBranchToDB(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) error {
-	cnt, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit)
-	if err != nil {
-		return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err)
-	}
-	if cnt > 0 { // This means branch does exist, so it's a normal update. It also means the branch has been synced.
-		return nil
+// SyncBranchesToDB sync the branch information in the database.
+// It will check whether the branches of the repository have never been synced before.
+// If so, it will sync all branches of the repository.
+// Otherwise, it will sync the branches that need to be updated.
+func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames, commitIDs []string, getCommit func(commitID string) (*git.Commit, error)) error {
+	// Some designs that make the code look strange but are made for performance optimization purposes:
+	// 1. Sync branches in a batch to reduce the number of DB queries.
+	// 2. Lazy load commit information since it may be not necessary.
+	// 3. Exit early if synced all branches of git repo when there's no branch in DB.
+	// 4. Check the branches in DB if they are already synced.
+	//
+	// If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once.
+	// See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27
+	// For the first batch, it will hit optimization 3.
+	// For other batches, it will hit optimization 4.
+
+	if len(branchNames) != len(commitIDs) {
+		return fmt.Errorf("branchNames and commitIDs length not match")
 	}
 
-	// if user haven't visit UI but directly push to a branch after upgrading from 1.20 -> 1.21,
-	// we cannot simply insert the branch but need to check we have branches or not
-	hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{
-		RepoID:          repoID,
-		IsDeletedBranch: util.OptionalBoolFalse,
-	}.ToConds())
-	if err != nil {
-		return err
-	}
-	if !hasBranch {
-		if _, err = repo_module.SyncRepoBranches(ctx, repoID, pusherID); err != nil {
-			return fmt.Errorf("repo_module.SyncRepoBranches %d:%s failed: %v", repoID, branchName, err)
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		branches, err := git_model.GetBranches(ctx, repoID, branchNames)
+		if err != nil {
+			return fmt.Errorf("git_model.GetBranches: %v", err)
+		}
+
+		if len(branches) == 0 {
+			// if user haven't visit UI but directly push to a branch after upgrading from 1.20 -> 1.21,
+			// we cannot simply insert the branch but need to check we have branches or not
+			hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{
+				RepoID:          repoID,
+				IsDeletedBranch: optional.Some(false),
+			}.ToConds())
+			if err != nil {
+				return err
+			}
+			if !hasBranch {
+				if _, err = repo_module.SyncRepoBranches(ctx, repoID, pusherID); err != nil {
+					return fmt.Errorf("repo_module.SyncRepoBranches %d failed: %v", repoID, err)
+				}
+				return nil
+			}
+		}
+
+		branchMap := make(map[string]*git_model.Branch, len(branches))
+		for _, branch := range branches {
+			branchMap[branch.Name] = branch
+		}
+
+		newBranches := make([]*git_model.Branch, 0, len(branchNames))
+
+		for i, branchName := range branchNames {
+			commitID := commitIDs[i]
+			branch, exist := branchMap[branchName]
+			if exist && branch.CommitID == commitID && !branch.IsDeleted {
+				continue
+			}
+
+			commit, err := getCommit(commitID)
+			if err != nil {
+				return fmt.Errorf("get commit of %s failed: %v", branchName, err)
+			}
+
+			if exist {
+				if _, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit); err != nil {
+					return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err)
+				}
+				return nil
+			}
+
+			// if database have branches but not this branch, it means this is a new branch
+			newBranches = append(newBranches, &git_model.Branch{
+				RepoID:        repoID,
+				Name:          branchName,
+				CommitID:      commit.ID.String(),
+				CommitMessage: commit.Summary(),
+				PusherID:      pusherID,
+				CommitTime:    timeutil.TimeStamp(commit.Committer.When.Unix()),
+			})
+		}
+
+		if len(newBranches) > 0 {
+			return db.Insert(ctx, newBranches)
 		}
 		return nil
-	}
-
-	// if database have branches but not this branch, it means this is a new branch
-	return db.Insert(ctx, &git_model.Branch{
-		RepoID:        repoID,
-		Name:          branchName,
-		CommitID:      commit.ID.String(),
-		CommitMessage: commit.Summary(),
-		PusherID:      pusherID,
-		CommitTime:    timeutil.TimeStamp(commit.Committer.When.Unix()),
 	})
 }
 
@@ -317,17 +410,17 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
 				log.Error("DeleteCronTaskByRepo: %v", err)
 			}
 			// cancel running cron jobs of this repository and delete old schedules
-			if err := actions_model.CancelRunningJobs(
+			if err := actions_model.CancelPreviousJobs(
 				ctx,
 				repo.ID,
 				from,
 				"",
 				webhook_module.HookEventSchedule,
 			); err != nil {
-				log.Error("CancelRunningJobs: %v", err)
+				log.Error("CancelPreviousJobs: %v", err)
 			}
 
-			err2 = gitRepo.SetDefaultBranch(to)
+			err2 = gitrepo.SetDefaultBranch(ctx, repo, to)
 			if err2 != nil {
 				return err2
 			}
@@ -378,11 +471,6 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
 		return fmt.Errorf("GetBranch: %vc", err)
 	}
 
-	objectFormat, err := gitRepo.GetObjectFormat()
-	if err != nil {
-		return err
-	}
-
 	if rawBranch.IsDeleted {
 		return nil
 	}
@@ -404,6 +492,8 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
 		return err
 	}
 
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+
 	// Don't return error below this
 	if err := PushUpdate(
 		&repo_module.PushUpdateOptions{
@@ -485,17 +575,17 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
 			log.Error("DeleteCronTaskByRepo: %v", err)
 		}
 		// cancel running cron jobs of this repository and delete old schedules
-		if err := actions_model.CancelRunningJobs(
+		if err := actions_model.CancelPreviousJobs(
 			ctx,
 			repo.ID,
 			oldDefaultBranchName,
 			"",
 			webhook_module.HookEventSchedule,
 		); err != nil {
-			log.Error("CancelRunningJobs: %v", err)
+			log.Error("CancelPreviousJobs: %v", err)
 		}
 
-		if err := gitRepo.SetDefaultBranch(newBranchName); err != nil {
+		if err := gitrepo.SetDefaultBranch(ctx, repo, newBranchName); err != nil {
 			if !git.IsErrUnsupportedVersion(err) {
 				return err
 			}
diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go
index dccc124748..4a43ae2a28 100644
--- a/services/repository/collaboration.go
+++ b/services/repository/collaboration.go
@@ -11,13 +11,14 @@ import (
 	"code.gitea.io/gitea/models/db"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
 )
 
 // DeleteCollaboration removes collaboration relation between the user and repository.
-func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid int64) (err error) {
+func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, collaborator *user_model.User) (err error) {
 	collaboration := &repo_model.Collaboration{
 		RepoID: repo.ID,
-		UserID: uid,
+		UserID: collaborator.ID,
 	}
 
 	ctx, committer, err := db.TxContext(ctx)
@@ -31,20 +32,25 @@ func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid i
 	} else if has == 0 {
 		return committer.Commit()
 	}
+
+	if err := repo.LoadOwner(ctx); err != nil {
+		return err
+	}
+
 	if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
 		return err
 	}
 
-	if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
+	if err = repo_model.WatchRepo(ctx, collaborator, repo, false); err != nil {
 		return err
 	}
 
-	if err = models.ReconsiderWatches(ctx, repo, uid); err != nil {
+	if err = models.ReconsiderWatches(ctx, repo, collaborator); err != nil {
 		return err
 	}
 
 	// Unassign a user from any issue (s)he has been assigned to in the repository
-	if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil {
+	if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, collaborator); err != nil {
 		return err
 	}
 
diff --git a/services/repository/collaboration_test.go b/services/repository/collaboration_test.go
index c3d006bfd8..a2eb06b81a 100644
--- a/services/repository/collaboration_test.go
+++ b/services/repository/collaboration_test.go
@@ -9,6 +9,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -16,13 +17,15 @@ import (
 func TestRepository_DeleteCollaboration(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
-	assert.NoError(t, repo.LoadOwner(db.DefaultContext))
-	assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4))
-	unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
 
-	assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4))
-	unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
+	assert.NoError(t, repo.LoadOwner(db.DefaultContext))
+	assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user))
+	unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID})
+
+	assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user))
+	unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID})
 
 	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
 }
diff --git a/services/repository/commit.go b/services/repository/commit.go
index 2497910a83..e8c0262ef4 100644
--- a/services/repository/commit.go
+++ b/services/repository/commit.go
@@ -7,8 +7,8 @@ import (
 	"context"
 	"fmt"
 
-	gitea_ctx "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/util"
+	gitea_ctx "code.gitea.io/gitea/services/context"
 )
 
 type ContainedLinks struct { // TODO: better name?
diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go
new file mode 100644
index 0000000000..7c1c6c2609
--- /dev/null
+++ b/services/repository/commitstatus/commitstatus.go
@@ -0,0 +1,199 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package commitstatus
+
+import (
+	"context"
+	"crypto/sha256"
+	"fmt"
+	"slices"
+
+	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/cache"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/log"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/automerge"
+)
+
+func getCacheKey(repoID int64, brancheName string) string {
+	hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%d:%s", repoID, brancheName)))
+	return fmt.Sprintf("commit_status:%x", hashBytes)
+}
+
+type commitStatusCacheValue struct {
+	State     string `json:"state"`
+	TargetURL string `json:"target_url"`
+}
+
+func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheValue {
+	c := cache.GetCache()
+	statusStr, ok := c.Get(getCacheKey(repoID, branchName)).(string)
+	if ok && statusStr != "" {
+		var cv commitStatusCacheValue
+		err := json.Unmarshal([]byte(statusStr), &cv)
+		if err == nil && cv.State != "" {
+			return &cv
+		}
+		if err != nil {
+			log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err)
+		}
+	}
+	return nil
+}
+
+func updateCommitStatusCache(repoID int64, branchName string, state api.CommitStatusState, targetURL string) error {
+	c := cache.GetCache()
+	bs, err := json.Marshal(commitStatusCacheValue{
+		State:     state.String(),
+		TargetURL: targetURL,
+	})
+	if err != nil {
+		log.Warn("updateCommitStatusCache: json.Marshal failed: %v", err)
+		return nil
+	}
+	return c.Put(getCacheKey(repoID, branchName), string(bs), 3*24*60)
+}
+
+func deleteCommitStatusCache(repoID int64, branchName string) error {
+	c := cache.GetCache()
+	return c.Delete(getCacheKey(repoID, branchName))
+}
+
+// CreateCommitStatus creates a new CommitStatus given a bunch of parameters
+// NOTE: All text-values will be trimmed from whitespaces.
+// Requires: Repo, Creator, SHA
+func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error {
+	repoPath := repo.RepoPath()
+
+	// confirm that commit is exist
+	gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
+	if err != nil {
+		return fmt.Errorf("OpenRepository[%s]: %w", repoPath, err)
+	}
+	defer closer.Close()
+
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+
+	commit, err := gitRepo.GetCommit(sha)
+	if err != nil {
+		return fmt.Errorf("GetCommit[%s]: %w", sha, err)
+	}
+	if len(sha) != objectFormat.FullLength() {
+		// use complete commit sha
+		sha = commit.ID.String()
+	}
+
+	if err := db.WithTx(ctx, func(ctx context.Context) error {
+		if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
+			Repo:         repo,
+			Creator:      creator,
+			SHA:          commit.ID,
+			CommitStatus: status,
+		}); err != nil {
+			return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
+		}
+
+		return git_model.UpdateCommitStatusSummary(ctx, repo.ID, commit.ID.String())
+	}); err != nil {
+		return err
+	}
+
+	defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
+	if err != nil {
+		return fmt.Errorf("GetBranchCommit[%s]: %w", repo.DefaultBranch, err)
+	}
+
+	if commit.ID.String() == defaultBranchCommit.ID.String() { // since one commit status updated, the combined commit status should be invalid
+		if err := deleteCommitStatusCache(repo.ID, repo.DefaultBranch); err != nil {
+			log.Error("deleteCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
+		}
+	}
+
+	if status.State.IsSuccess() {
+		if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil {
+			return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
+		}
+	}
+
+	return nil
+}
+
+// FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache
+func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) {
+	results := make([]*git_model.CommitStatus, len(repos))
+	for i, repo := range repos {
+		if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil {
+			results[i] = &git_model.CommitStatus{
+				State:     api.CommitStatusState(cv.State),
+				TargetURL: cv.TargetURL,
+			}
+		}
+	}
+
+	// collect the latest commit of each repo
+	// at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment
+	repoBranchNames := make(map[int64]string, len(repos))
+	for i, repo := range repos {
+		if results[i] == nil {
+			repoBranchNames[repo.ID] = repo.DefaultBranch
+		}
+	}
+
+	repoIDsToLatestCommitSHAs, err := git_model.FindBranchesByRepoAndBranchName(ctx, repoBranchNames)
+	if err != nil {
+		return nil, fmt.Errorf("FindBranchesByRepoAndBranchName: %v", err)
+	}
+
+	var repoSHAs []git_model.RepoSHA
+	for id, sha := range repoIDsToLatestCommitSHAs {
+		repoSHAs = append(repoSHAs, git_model.RepoSHA{RepoID: id, SHA: sha})
+	}
+
+	summaryResults, err := git_model.GetLatestCommitStatusForRepoAndSHAs(ctx, repoSHAs)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestCommitStatusForRepoAndSHAs: %v", err)
+	}
+
+	for _, summary := range summaryResults {
+		for i, repo := range repos {
+			if repo.ID == summary.RepoID {
+				results[i] = summary
+				_ = slices.DeleteFunc(repoSHAs, func(repoSHA git_model.RepoSHA) bool {
+					return repoSHA.RepoID == repo.ID
+				})
+				if results[i].State != "" {
+					if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
+						log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
+					}
+				}
+				break
+			}
+		}
+	}
+
+	// call the database O(1) times to get the commit statuses for all repos
+	repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoSHAs)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestCommitStatusForPairs: %v", err)
+	}
+
+	for i, repo := range repos {
+		if results[i] == nil {
+			results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
+			if results[i].State != "" {
+				if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
+					log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
+				}
+			}
+		}
+	}
+
+	return results, nil
+}
diff --git a/services/repository/contributors_graph.go b/services/repository/contributors_graph.go
new file mode 100644
index 0000000000..7c9f535ae0
--- /dev/null
+++ b/services/repository/contributors_graph.go
@@ -0,0 +1,317 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+	"bufio"
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"code.gitea.io/gitea/models/avatars"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/graceful"
+	"code.gitea.io/gitea/modules/log"
+	api "code.gitea.io/gitea/modules/structs"
+
+	"gitea.com/go-chi/cache"
+)
+
+const (
+	contributorStatsCacheKey           = "GetContributorStats/%s/%s"
+	contributorStatsCacheTimeout int64 = 60 * 10
+)
+
+var (
+	ErrAwaitGeneration  = errors.New("generation took longer than ")
+	awaitGenerationTime = time.Second * 5
+	generateLock        = sync.Map{}
+)
+
+type WeekData struct {
+	Week      int64 `json:"week"`      // Starting day of the week as Unix timestamp
+	Additions int   `json:"additions"` // Number of additions in that week
+	Deletions int   `json:"deletions"` // Number of deletions in that week
+	Commits   int   `json:"commits"`   // Number of commits in that week
+}
+
+// ContributorData represents statistical git commit count data
+type ContributorData struct {
+	Name         string              `json:"name"`  // Display name of the contributor
+	Login        string              `json:"login"` // Login name of the contributor in case it exists
+	AvatarLink   string              `json:"avatar_link"`
+	HomeLink     string              `json:"home_link"`
+	TotalCommits int64               `json:"total_commits"`
+	Weeks        map[int64]*WeekData `json:"weeks"`
+}
+
+// ExtendedCommitStats contains information for commit stats with author data
+type ExtendedCommitStats struct {
+	Author *api.CommitUser  `json:"author"`
+	Stats  *api.CommitStats `json:"stats"`
+}
+
+const layout = time.DateOnly
+
+func findLastSundayBeforeDate(dateStr string) (string, error) {
+	date, err := time.Parse(layout, dateStr)
+	if err != nil {
+		return "", err
+	}
+
+	weekday := date.Weekday()
+	daysToSubtract := int(weekday) - int(time.Sunday)
+	if daysToSubtract < 0 {
+		daysToSubtract += 7
+	}
+
+	lastSunday := date.AddDate(0, 0, -daysToSubtract)
+	return lastSunday.Format(layout), nil
+}
+
+// GetContributorStats returns contributors stats for git commits for given revision or default branch
+func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) {
+	// as GetContributorStats is resource intensive we cache the result
+	cacheKey := fmt.Sprintf(contributorStatsCacheKey, repo.FullName(), revision)
+	if !cache.IsExist(cacheKey) {
+		genReady := make(chan struct{})
+
+		// dont start multible async generations
+		_, run := generateLock.Load(cacheKey)
+		if run {
+			return nil, ErrAwaitGeneration
+		}
+
+		generateLock.Store(cacheKey, struct{}{})
+		// run generation async
+		go generateContributorStats(genReady, cache, cacheKey, repo, revision)
+
+		select {
+		case <-time.After(awaitGenerationTime):
+			return nil, ErrAwaitGeneration
+		case <-genReady:
+			// we got generation ready before timeout
+			break
+		}
+	}
+	// TODO: renew timeout of cache cache.UpdateTimeout(cacheKey, contributorStatsCacheTimeout)
+
+	switch v := cache.Get(cacheKey).(type) {
+	case error:
+		return nil, v
+	case map[string]*ContributorData:
+		return v, nil
+	default:
+		return nil, fmt.Errorf("unexpected type in cache detected")
+	}
+}
+
+// getExtendedCommitStats return the list of *ExtendedCommitStats for the given revision
+func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int */) ([]*ExtendedCommitStats, error) {
+	baseCommit, err := repo.GetCommit(revision)
+	if err != nil {
+		return nil, err
+	}
+	stdoutReader, stdoutWriter, err := os.Pipe()
+	if err != nil {
+		return nil, err
+	}
+	defer func() {
+		_ = stdoutReader.Close()
+		_ = stdoutWriter.Close()
+	}()
+
+	gitCmd := git.NewCommand(repo.Ctx, "log", "--shortstat", "--no-merges", "--pretty=format:---%n%aN%n%aE%n%as", "--reverse")
+	// AddOptionFormat("--max-count=%d", limit)
+	gitCmd.AddDynamicArguments(baseCommit.ID.String())
+
+	var extendedCommitStats []*ExtendedCommitStats
+	stderr := new(strings.Builder)
+	err = gitCmd.Run(&git.RunOpts{
+		Dir:    repo.Path,
+		Stdout: stdoutWriter,
+		Stderr: stderr,
+		PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
+			_ = stdoutWriter.Close()
+			scanner := bufio.NewScanner(stdoutReader)
+
+			for scanner.Scan() {
+				line := strings.TrimSpace(scanner.Text())
+				if line != "---" {
+					continue
+				}
+				scanner.Scan()
+				authorName := strings.TrimSpace(scanner.Text())
+				scanner.Scan()
+				authorEmail := strings.TrimSpace(scanner.Text())
+				scanner.Scan()
+				date := strings.TrimSpace(scanner.Text())
+				scanner.Scan()
+				stats := strings.TrimSpace(scanner.Text())
+				if authorName == "" || authorEmail == "" || date == "" || stats == "" {
+					// FIXME: find a better way to parse the output so that we will handle this properly
+					log.Warn("Something is wrong with git log output, skipping...")
+					log.Warn("authorName: %s,  authorEmail: %s,  date: %s,  stats: %s", authorName, authorEmail, date, stats)
+					continue
+				}
+				//  1 file changed, 1 insertion(+), 1 deletion(-)
+				fields := strings.Split(stats, ",")
+
+				commitStats := api.CommitStats{}
+				for _, field := range fields[1:] {
+					parts := strings.Split(strings.TrimSpace(field), " ")
+					value, contributionType := parts[0], parts[1]
+					amount, _ := strconv.Atoi(value)
+
+					if strings.HasPrefix(contributionType, "insertion") {
+						commitStats.Additions = amount
+					} else {
+						commitStats.Deletions = amount
+					}
+				}
+				commitStats.Total = commitStats.Additions + commitStats.Deletions
+				scanner.Text() // empty line at the end
+
+				res := &ExtendedCommitStats{
+					Author: &api.CommitUser{
+						Identity: api.Identity{
+							Name:  authorName,
+							Email: authorEmail,
+						},
+						Date: date,
+					},
+					Stats: &commitStats,
+				}
+				extendedCommitStats = append(extendedCommitStats, res)
+
+			}
+			_ = stdoutReader.Close()
+			return nil
+		},
+	})
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get ContributorsCommitStats for repository.\nError: %w\nStderr: %s", err, stderr)
+	}
+
+	return extendedCommitStats, nil
+}
+
+func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey string, repo *repo_model.Repository, revision string) {
+	ctx := graceful.GetManager().HammerContext()
+
+	gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
+	if err != nil {
+		err := fmt.Errorf("OpenRepository: %w", err)
+		_ = cache.Put(cacheKey, err, contributorStatsCacheTimeout)
+		return
+	}
+	defer closer.Close()
+
+	if len(revision) == 0 {
+		revision = repo.DefaultBranch
+	}
+	extendedCommitStats, err := getExtendedCommitStats(gitRepo, revision)
+	if err != nil {
+		err := fmt.Errorf("ExtendedCommitStats: %w", err)
+		_ = cache.Put(cacheKey, err, contributorStatsCacheTimeout)
+		return
+	}
+	if len(extendedCommitStats) == 0 {
+		err := fmt.Errorf("no commit stats returned for revision '%s'", revision)
+		_ = cache.Put(cacheKey, err, contributorStatsCacheTimeout)
+		return
+	}
+
+	layout := time.DateOnly
+
+	unknownUserAvatarLink := user_model.NewGhostUser().AvatarLinkWithSize(ctx, 0)
+	contributorsCommitStats := make(map[string]*ContributorData)
+	contributorsCommitStats["total"] = &ContributorData{
+		Name:  "Total",
+		Weeks: make(map[int64]*WeekData),
+	}
+	total := contributorsCommitStats["total"]
+
+	for _, v := range extendedCommitStats {
+		userEmail := v.Author.Email
+		if len(userEmail) == 0 {
+			continue
+		}
+		u, _ := user_model.GetUserByEmail(ctx, userEmail)
+		if u != nil {
+			// update userEmail with user's primary email address so
+			// that different mail addresses will linked to same account
+			userEmail = u.GetEmail()
+		}
+		// duplicated logic
+		if _, ok := contributorsCommitStats[userEmail]; !ok {
+			if u == nil {
+				avatarLink := avatars.GenerateEmailAvatarFastLink(ctx, userEmail, 0)
+				if avatarLink == "" {
+					avatarLink = unknownUserAvatarLink
+				}
+				contributorsCommitStats[userEmail] = &ContributorData{
+					Name:       v.Author.Name,
+					AvatarLink: avatarLink,
+					Weeks:      make(map[int64]*WeekData),
+				}
+			} else {
+				contributorsCommitStats[userEmail] = &ContributorData{
+					Name:       u.DisplayName(),
+					Login:      u.LowerName,
+					AvatarLink: u.AvatarLinkWithSize(ctx, 0),
+					HomeLink:   u.HomeLink(),
+					Weeks:      make(map[int64]*WeekData),
+				}
+			}
+		}
+		// Update user statistics
+		user := contributorsCommitStats[userEmail]
+		startingOfWeek, _ := findLastSundayBeforeDate(v.Author.Date)
+
+		val, _ := time.Parse(layout, startingOfWeek)
+		week := val.UnixMilli()
+
+		if user.Weeks[week] == nil {
+			user.Weeks[week] = &WeekData{
+				Additions: 0,
+				Deletions: 0,
+				Commits:   0,
+				Week:      week,
+			}
+		}
+		if total.Weeks[week] == nil {
+			total.Weeks[week] = &WeekData{
+				Additions: 0,
+				Deletions: 0,
+				Commits:   0,
+				Week:      week,
+			}
+		}
+		user.Weeks[week].Additions += v.Stats.Additions
+		user.Weeks[week].Deletions += v.Stats.Deletions
+		user.Weeks[week].Commits++
+		user.TotalCommits++
+
+		// Update overall statistics
+		total.Weeks[week].Additions += v.Stats.Additions
+		total.Weeks[week].Deletions += v.Stats.Deletions
+		total.Weeks[week].Commits++
+		total.TotalCommits++
+	}
+
+	_ = cache.Put(cacheKey, contributorsCommitStats, contributorStatsCacheTimeout)
+	generateLock.Delete(cacheKey)
+	if genDone != nil {
+		genDone <- struct{}{}
+	}
+}
diff --git a/services/repository/contributors_graph_test.go b/services/repository/contributors_graph_test.go
new file mode 100644
index 0000000000..3801a5eee4
--- /dev/null
+++ b/services/repository/contributors_graph_test.go
@@ -0,0 +1,87 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+	"slices"
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/modules/git"
+
+	"gitea.com/go-chi/cache"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRepository_ContributorsGraph(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+	assert.NoError(t, repo.LoadOwner(db.DefaultContext))
+	mockCache, err := cache.NewCacher(cache.Options{
+		Adapter:  "memory",
+		Interval: 24 * 60,
+	})
+	assert.NoError(t, err)
+
+	generateContributorStats(nil, mockCache, "key", repo, "404ref")
+	err, isErr := mockCache.Get("key").(error)
+	assert.True(t, isErr)
+	assert.ErrorAs(t, err, &git.ErrNotExist{})
+
+	generateContributorStats(nil, mockCache, "key2", repo, "master")
+	data, isData := mockCache.Get("key2").(map[string]*ContributorData)
+	assert.True(t, isData)
+	var keys []string
+	for k := range data {
+		keys = append(keys, k)
+	}
+	slices.Sort(keys)
+	assert.EqualValues(t, []string{
+		"ethantkoenig@gmail.com",
+		"jimmy.praet@telenet.be",
+		"jon@allspice.io",
+		"total", // generated summary
+	}, keys)
+
+	assert.EqualValues(t, &ContributorData{
+		Name:         "Ethan Koenig",
+		AvatarLink:   "https://secure.gravatar.com/avatar/b42fb195faa8c61b8d88abfefe30e9e3?d=identicon",
+		TotalCommits: 1,
+		Weeks: map[int64]*WeekData{
+			1511654400000: {
+				Week:      1511654400000, // sunday 2017-11-26
+				Additions: 3,
+				Deletions: 0,
+				Commits:   1,
+			},
+		},
+	}, data["ethantkoenig@gmail.com"])
+	assert.EqualValues(t, &ContributorData{
+		Name:         "Total",
+		AvatarLink:   "",
+		TotalCommits: 3,
+		Weeks: map[int64]*WeekData{
+			1511654400000: {
+				Week:      1511654400000, // sunday 2017-11-26 (2017-11-26 20:31:18 -0800)
+				Additions: 3,
+				Deletions: 0,
+				Commits:   1,
+			},
+			1607817600000: {
+				Week:      1607817600000, // sunday 2020-12-13 (2020-12-15 15:23:11 -0500)
+				Additions: 10,
+				Deletions: 0,
+				Commits:   1,
+			},
+			1624752000000: {
+				Week:      1624752000000, // sunday 2021-06-27 (2021-06-29 21:54:09 +0200)
+				Additions: 2,
+				Deletions: 0,
+				Commits:   1,
+			},
+		},
+	}, data["total"])
+}
diff --git a/services/repository/create.go b/services/repository/create.go
index d5942e6c07..c3743cacba 100644
--- a/services/repository/create.go
+++ b/services/repository/create.go
@@ -158,7 +158,7 @@ func initRepository(ctx context.Context, u *user_model.User, repo *repo_model.Re
 		}
 
 		// Apply changes and commit.
-		if err = repo_module.InitRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil {
+		if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil {
 			return fmt.Errorf("initRepoCommit: %w", err)
 		}
 	}
@@ -174,15 +174,11 @@ func initRepository(ctx context.Context, u *user_model.User, repo *repo_model.Re
 	}
 
 	repo.DefaultBranch = setting.Repository.DefaultBranch
+	repo.DefaultWikiBranch = setting.Repository.DefaultBranch
 
 	if len(opts.DefaultBranch) > 0 {
 		repo.DefaultBranch = opts.DefaultBranch
-		gitRepo, err := gitrepo.OpenRepository(ctx, repo)
-		if err != nil {
-			return fmt.Errorf("openRepository: %w", err)
-		}
-		defer gitRepo.Close()
-		if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+		if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
 			return fmt.Errorf("setDefaultBranch: %w", err)
 		}
 
@@ -241,6 +237,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt
 		TrustModel:                      opts.TrustModel,
 		IsMirror:                        opts.IsMirror,
 		DefaultBranch:                   opts.DefaultBranch,
+		DefaultWikiBranch:               setting.Repository.DefaultBranch,
 		ObjectFormatName:                opts.ObjectFormatName,
 	}
 
diff --git a/services/repository/create_test.go b/services/repository/create_test.go
index b3e1f0550c..41e6b615db 100644
--- a/services/repository/create_test.go
+++ b/services/repository/create_test.go
@@ -21,12 +21,12 @@ import (
 func TestIncludesAllRepositoriesTeams(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	testTeamRepositories := func(teamID int64, repoIds []int64) {
+	testTeamRepositories := func(teamID int64, repoIDs []int64) {
 		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
 		assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name)
 		assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name)
-		assert.Len(t, team.Repos, len(repoIds), "%s: repo count", team.Name)
-		for i, rid := range repoIds {
+		assert.Len(t, team.Repos, len(repoIDs), "%s: repo count", team.Name)
+		for i, rid := range repoIDs {
 			if rid > 0 {
 				assert.True(t, HasRepository(db.DefaultContext, team, rid), "%s: HasRepository(%d) %d", rid, i)
 			}
@@ -52,12 +52,12 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
 	assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories")
 
 	// Create repos.
-	repoIds := make([]int64, 0)
+	repoIDs := make([]int64, 0)
 	for i := 0; i < 3; i++ {
 		r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)})
 		assert.NoError(t, err, "CreateRepository %d", i)
 		if r != nil {
-			repoIds = append(repoIds, r.ID)
+			repoIDs = append(repoIDs, r.ID)
 		}
 	}
 	// Get fresh copy of Owner team after creating repos.
@@ -93,10 +93,10 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
 		},
 	}
 	teamRepos := [][]int64{
-		repoIds,
-		repoIds,
+		repoIDs,
+		repoIDs,
 		{},
-		repoIds,
+		repoIDs,
 		{},
 	}
 	for i, team := range teams {
@@ -109,7 +109,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
 	// Update teams and check repositories.
 	teams[3].IncludesAllRepositories = false
 	teams[4].IncludesAllRepositories = true
-	teamRepos[4] = repoIds
+	teamRepos[4] = repoIDs
 	for i, team := range teams {
 		assert.NoError(t, models.UpdateTeam(db.DefaultContext, team, false, true), "%s: UpdateTeam", team.Name)
 		testTeamRepositories(team.ID, teamRepos[i])
@@ -119,27 +119,27 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
 	r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: "repo-last"})
 	assert.NoError(t, err, "CreateRepository last")
 	if r != nil {
-		repoIds = append(repoIds, r.ID)
+		repoIDs = append(repoIDs, r.ID)
 	}
-	teamRepos[0] = repoIds
-	teamRepos[1] = repoIds
-	teamRepos[4] = repoIds
+	teamRepos[0] = repoIDs
+	teamRepos[1] = repoIDs
+	teamRepos[4] = repoIDs
 	for i, team := range teams {
 		testTeamRepositories(team.ID, teamRepos[i])
 	}
 
 	// Remove repo and check teams repositories.
-	assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, repoIds[0]), "DeleteRepository")
-	teamRepos[0] = repoIds[1:]
-	teamRepos[1] = repoIds[1:]
-	teamRepos[3] = repoIds[1:3]
-	teamRepos[4] = repoIds[1:]
+	assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, repoIDs[0]), "DeleteRepository")
+	teamRepos[0] = repoIDs[1:]
+	teamRepos[1] = repoIDs[1:]
+	teamRepos[3] = repoIDs[1:3]
+	teamRepos[4] = repoIDs[1:]
 	for i, team := range teams {
 		testTeamRepositories(team.ID, teamRepos[i])
 	}
 
 	// Wipe created items.
-	for i, rid := range repoIds {
+	for i, rid := range repoIDs {
 		if i > 0 { // first repo already deleted.
 			assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, rid), "DeleteRepository %d", i)
 		}
diff --git a/services/repository/delete.go b/services/repository/delete.go
index 426b4dbead..803090cccf 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -28,6 +28,7 @@ import (
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/storage"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 
 	"xorm.io/builder"
 )
@@ -163,6 +164,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
 		&actions_model.ActionScheduleSpec{RepoID: repoID},
 		&actions_model.ActionSchedule{RepoID: repoID},
 		&actions_model.ActionArtifact{RepoID: repoID},
+		&actions_model.ActionRunnerToken{RepoID: repoID},
 	); err != nil {
 		return fmt.Errorf("deleteBeans: %w", err)
 	}
@@ -278,7 +280,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
 	committer.Close()
 
 	if needRewriteKeysFile {
-		if err := asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+		if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
 			log.Error("RewriteAllPublicKeys failed: %v", err)
 		}
 	}
@@ -365,24 +367,26 @@ func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *r
 		}
 	}
 
-	teamUsers, err := organization.GetTeamUsersByTeamID(ctx, t.ID)
+	teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
+		TeamID: t.ID,
+	})
 	if err != nil {
-		return fmt.Errorf("getTeamUsersByTeamID: %w", err)
+		return fmt.Errorf("GetTeamMembers: %w", err)
 	}
-	for _, teamUser := range teamUsers {
-		has, err := access_model.HasAccess(ctx, teamUser.UID, repo)
+	for _, member := range teamMembers {
+		has, err := access_model.HasAccess(ctx, member.ID, repo)
 		if err != nil {
 			return err
 		} else if has {
 			continue
 		}
 
-		if err = repo_model.WatchRepo(ctx, teamUser.UID, repo.ID, false); err != nil {
+		if err = repo_model.WatchRepo(ctx, member, repo, false); err != nil {
 			return err
 		}
 
 		// Remove all IssueWatches a user has subscribed to in the repositories
-		if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil {
+		if err := issues_model.RemoveIssueWatchersByRepoID(ctx, member.ID, repo.ID); err != nil {
 			return err
 		}
 	}
diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go
index 613b46d8f6..451a182155 100644
--- a/services/repository/files/cherry_pick.go
+++ b/services/repository/files/cherry_pick.go
@@ -28,7 +28,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
 
 	t, err := NewTemporaryUploadRepository(ctx, repo)
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("NewTemporaryUploadRepository failed: %v", err)
 	}
 	defer t.Close()
 	if err := t.Clone(opts.OldBranch, false); err != nil {
diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go
index 937db472e6..e0dad29273 100644
--- a/services/repository/files/commit.go
+++ b/services/repository/files/commit.go
@@ -5,59 +5,20 @@ package files
 
 import (
 	"context"
-	"fmt"
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
-	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
-	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/services/automerge"
 )
 
-// CreateCommitStatus creates a new CommitStatus given a bunch of parameters
-// NOTE: All text-values will be trimmed from whitespaces.
-// Requires: Repo, Creator, SHA
-func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error {
-	// confirm that commit is exist
-	gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
+// CountDivergingCommits determines how many commits a branch is ahead or behind the repository's base branch
+func CountDivergingCommits(ctx context.Context, repo *repo_model.Repository, branch string) (*git.DivergeObject, error) {
+	divergence, err := git.GetDivergingCommits(ctx, repo.RepoPath(), repo.DefaultBranch, branch)
 	if err != nil {
-		return fmt.Errorf("OpenRepository[%s]: %w", gitrepo.RepoGitURL(repo), err)
+		return nil, err
 	}
-	defer closer.Close()
-
-	objectFormat, err := gitRepo.GetObjectFormat()
-	if err != nil {
-		return fmt.Errorf("GetObjectFormat[%s]: %w", gitrepo.RepoGitURL(repo), err)
-	}
-	commit, err := gitRepo.GetCommit(sha)
-	if err != nil {
-		gitRepo.Close()
-		return fmt.Errorf("GetCommit[%s]: %w", sha, err)
-	} else if len(sha) != objectFormat.FullLength() {
-		// use complete commit sha
-		sha = commit.ID.String()
-	}
-	gitRepo.Close()
-
-	if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
-		Repo:         repo,
-		Creator:      creator,
-		SHA:          commit.ID,
-		CommitStatus: status,
-	}); err != nil {
-		return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
-	}
-
-	if status.State.IsSuccess() {
-		if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil {
-			return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
-		}
-	}
-
-	return nil
+	return &divergence, nil
 }
 
 // GetPayloadCommitVerification returns the verification information of a commit
diff --git a/services/repository/files/content.go b/services/repository/files/content.go
index c278d7f835..95e7c7087c 100644
--- a/services/repository/files/content.go
+++ b/services/repository/files/content.go
@@ -220,7 +220,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
 		}
 	}
 	// Handle links
-	if entry.IsRegular() || entry.IsLink() {
+	if entry.IsRegular() || entry.IsLink() || entry.IsExecutable() {
 		downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath))
 		if err != nil {
 			return nil, err
@@ -270,3 +270,28 @@ func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
 		Content:  content,
 	}, nil
 }
+
+// TryGetContentLanguage tries to get the (linguist) language of the file content
+func TryGetContentLanguage(gitRepo *git.Repository, commitID, treePath string) (string, error) {
+	indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(commitID)
+	if err != nil {
+		return "", err
+	}
+
+	defer deleteTemporaryFile()
+
+	filename2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
+		CachedOnly: true,
+		Attributes: []string{git.AttributeLinguistLanguage, git.AttributeGitlabLanguage},
+		Filenames:  []string{treePath},
+		IndexFile:  indexFilename,
+		WorkTree:   worktree,
+	})
+	if err != nil {
+		return "", err
+	}
+
+	language := git.TryReadLanguageAttribute(filename2attribute2info[treePath])
+
+	return language.Value(), nil
+}
diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go
index d50847789a..4811f9d327 100644
--- a/services/repository/files/content_test.go
+++ b/services/repository/files/content_test.go
@@ -7,9 +7,9 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/gitrepo"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/contexttest"
 
 	_ "code.gitea.io/gitea/models/actions"
 
diff --git a/services/repository/files/diff_test.go b/services/repository/files/diff_test.go
index 91c878e505..63aff9b0e3 100644
--- a/services/repository/files/diff_test.go
+++ b/services/repository/files/diff_test.go
@@ -8,8 +8,8 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/services/contexttest"
 	"code.gitea.io/gitea/services/gitdiff"
 
 	"github.com/stretchr/testify/assert"
diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go
index 675ddbddb3..a5b3aad91e 100644
--- a/services/repository/files/file_test.go
+++ b/services/repository/files/file_test.go
@@ -7,10 +7,10 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go
index f6d5643dc9..e5f7e2af96 100644
--- a/services/repository/files/patch.go
+++ b/services/repository/files/patch.go
@@ -111,7 +111,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
 
 	t, err := NewTemporaryUploadRepository(ctx, repo)
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("NewTemporaryUploadRepository failed: %v", err)
 	}
 	defer t.Close()
 	if err := t.Clone(opts.OldBranch, true); err != nil {
diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go
index 9d3185c3fc..e3a7f3b8b0 100644
--- a/services/repository/files/tree.go
+++ b/services/repository/files/tree.go
@@ -37,7 +37,7 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
 	}
 	apiURL := repo.APIURL()
 	apiURLLen := len(apiURL)
-	objectFormat, _ := gitRepo.GetObjectFormat()
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
 	hashLen := objectFormat.FullLength()
 
 	const gitBlobsPath = "/git/blobs/"
diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go
index 528ef500df..508f20090d 100644
--- a/services/repository/files/tree_test.go
+++ b/services/repository/files/tree_test.go
@@ -7,8 +7,8 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/contexttest"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/services/repository/files/update.go b/services/repository/files/update.go
index f223daf3a9..f029a9aefe 100644
--- a/services/repository/files/update.go
+++ b/services/repository/files/update.go
@@ -40,7 +40,7 @@ type ChangeRepoFile struct {
 	Operation     string
 	TreePath      string
 	FromTreePath  string
-	ContentReader io.Reader
+	ContentReader io.ReadSeeker
 	SHA           string
 	Options       *RepoFileOptions
 }
@@ -143,7 +143,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
 
 	t, err := NewTemporaryUploadRepository(ctx, repo)
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("NewTemporaryUploadRepository failed: %v", err)
 	}
 	defer t.Close()
 	hasOldBranch := true
@@ -448,6 +448,10 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file
 			return err
 		}
 		if !exist {
+			_, err := file.ContentReader.Seek(0, io.SeekStart)
+			if err != nil {
+				return err
+			}
 			if err := contentStore.Put(lfsMetaObject.Pointer, file.ContentReader); err != nil {
 				if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil {
 					return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err)
diff --git a/services/repository/fork.go b/services/repository/fork.go
index 1c532dd70e..6b1e16b1c3 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -52,6 +52,14 @@ type ForkRepoOptions struct {
 
 // ForkRepository forks a repository
 func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts ForkRepoOptions) (*repo_model.Repository, error) {
+	if err := opts.BaseRepo.LoadOwner(ctx); err != nil {
+		return nil, err
+	}
+
+	if user_model.IsUserBlockedBy(ctx, doer, opts.BaseRepo.Owner.ID) {
+		return nil, user_model.ErrBlockedUser
+	}
+
 	// Fork is prohibited, if user has reached maximum limit of repositories
 	if !owner.CanForkRepo() {
 		return nil, repo_model.ErrReachLimitOfRepo{
diff --git a/modules/repository/generate.go b/services/repository/generate.go
similarity index 93%
rename from modules/repository/generate.go
rename to services/repository/generate.go
index d7bb5bab30..cf05ab28a7 100644
--- a/modules/repository/generate.go
+++ b/services/repository/generate.go
@@ -21,6 +21,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
+	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/util"
 
 	"github.com/gobwas/glob"
@@ -94,7 +95,7 @@ type GiteaTemplate struct {
 }
 
 // Globs parses the .gitea/template globs or returns them if they were already parsed
-func (gt GiteaTemplate) Globs() []glob.Glob {
+func (gt *GiteaTemplate) Globs() []glob.Glob {
 	if gt.globs != nil {
 		return gt.globs
 	}
@@ -242,7 +243,7 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
 		defaultBranch = templateRepo.DefaultBranch
 	}
 
-	return InitRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
+	return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
 }
 
 func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) {
@@ -271,12 +272,7 @@ func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *r
 		repo.DefaultBranch = templateRepo.DefaultBranch
 	}
 
-	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
-	if err != nil {
-		return fmt.Errorf("openRepository: %w", err)
-	}
-	defer gitRepo.Close()
-	if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+	if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
 		return fmt.Errorf("setDefaultBranch: %w", err)
 	}
 	if err = UpdateRepository(ctx, repo, false); err != nil {
@@ -292,7 +288,7 @@ func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_mo
 		return err
 	}
 
-	if err := UpdateRepoSize(ctx, generateRepo); err != nil {
+	if err := repo_module.UpdateRepoSize(ctx, generateRepo); err != nil {
 		return fmt.Errorf("failed to update size for repository: %w", err)
 	}
 
@@ -323,8 +319,8 @@ func (gro GenerateRepoOptions) IsValid() bool {
 		gro.IssueLabels || gro.ProtectedBranch // or other items as they are added
 }
 
-// GenerateRepository generates a repository from a template
-func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) {
+// generateRepository generates a repository from a template
+func generateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) {
 	generateRepo := &repo_model.Repository{
 		OwnerID:          owner.ID,
 		Owner:            owner,
@@ -341,7 +337,7 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
 		ObjectFormatName: templateRepo.ObjectFormatName,
 	}
 
-	if err = CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil {
+	if err = repo_module.CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil {
 		return nil, err
 	}
 
@@ -358,11 +354,11 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
 		}
 	}
 
-	if err = CheckInitRepository(ctx, generateRepo, generateRepo.ObjectFormatName); err != nil {
+	if err = repo_module.CheckInitRepository(ctx, generateRepo, generateRepo.ObjectFormatName); err != nil {
 		return generateRepo, err
 	}
 
-	if err = CheckDaemonExportOK(ctx, generateRepo); err != nil {
+	if err = repo_module.CheckDaemonExportOK(ctx, generateRepo); err != nil {
 		return generateRepo, fmt.Errorf("checkDaemonExportOK: %w", err)
 	}
 
diff --git a/modules/repository/generate_test.go b/services/repository/generate_test.go
similarity index 100%
rename from modules/repository/generate_test.go
rename to services/repository/generate_test.go
diff --git a/services/repository/init.go b/services/repository/init.go
new file mode 100644
index 0000000000..817fa4abd7
--- /dev/null
+++ b/services/repository/init.go
@@ -0,0 +1,83 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"time"
+
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
+	repo_module "code.gitea.io/gitea/modules/repository"
+	"code.gitea.io/gitea/modules/setting"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
+)
+
+// initRepoCommit temporarily changes with work directory.
+func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) {
+	commitTimeStr := time.Now().Format(time.RFC3339)
+
+	sig := u.NewGitSig()
+	// Because this may call hooks we should pass in the environment
+	env := append(os.Environ(),
+		"GIT_AUTHOR_NAME="+sig.Name,
+		"GIT_AUTHOR_EMAIL="+sig.Email,
+		"GIT_AUTHOR_DATE="+commitTimeStr,
+		"GIT_COMMITTER_DATE="+commitTimeStr,
+	)
+	committerName := sig.Name
+	committerEmail := sig.Email
+
+	if stdout, _, err := git.NewCommand(ctx, "add", "--all").
+		SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)).
+		RunStdString(&git.RunOpts{Dir: tmpPath}); err != nil {
+		log.Error("git add --all failed: Stdout: %s\nError: %v", stdout, err)
+		return fmt.Errorf("git add --all: %w", err)
+	}
+
+	cmd := git.NewCommand(ctx, "commit", "--message=Initial commit").
+		AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
+
+	sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)
+	if sign {
+		cmd.AddOptionFormat("-S%s", keyID)
+
+		if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
+			// need to set the committer to the KeyID owner
+			committerName = signer.Name
+			committerEmail = signer.Email
+		}
+	} else {
+		cmd.AddArguments("--no-gpg-sign")
+	}
+
+	env = append(env,
+		"GIT_COMMITTER_NAME="+committerName,
+		"GIT_COMMITTER_EMAIL="+committerEmail,
+	)
+
+	if stdout, _, err := cmd.
+		SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)).
+		RunStdString(&git.RunOpts{Dir: tmpPath, Env: env}); err != nil {
+		log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.String(), stdout, err)
+		return fmt.Errorf("git commit: %w", err)
+	}
+
+	if len(defaultBranch) == 0 {
+		defaultBranch = setting.Repository.DefaultBranch
+	}
+
+	if stdout, _, err := git.NewCommand(ctx, "push", "origin").AddDynamicArguments("HEAD:" + defaultBranch).
+		SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)).
+		RunStdString(&git.RunOpts{Dir: tmpPath, Env: repo_module.InternalPushingEnvironment(u, repo)}); err != nil {
+		log.Error("Failed to push back to HEAD: Stdout: %s\nError: %v", stdout, err)
+		return fmt.Errorf("git push: %w", err)
+	}
+
+	return nil
+}
diff --git a/services/repository/lfs.go b/services/repository/lfs.go
index 4504f796bd..4d48881b87 100644
--- a/services/repository/lfs.go
+++ b/services/repository/lfs.go
@@ -79,7 +79,7 @@ func GarbageCollectLFSMetaObjectsForRepo(ctx context.Context, repo *repo_model.R
 
 	store := lfs.NewContentStore()
 	errStop := errors.New("STOPERR")
-	objectFormat, _ := gitRepo.GetObjectFormat()
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
 
 	err = git_model.IterateLFSMetaObjectsForRepo(ctx, repo.ID, func(ctx context.Context, metaObject *git_model.LFSMetaObject, count int64) error {
 		if opts.NumberToCheckPerRepo > 0 && total > opts.NumberToCheckPerRepo {
diff --git a/services/repository/migrate.go b/services/repository/migrate.go
new file mode 100644
index 0000000000..cbfc14a39c
--- /dev/null
+++ b/services/repository/migrate.go
@@ -0,0 +1,286 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/organization"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/lfs"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/migration"
+	repo_module "code.gitea.io/gitea/modules/repository"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
+)
+
+func cloneWiki(ctx context.Context, u *user_model.User, opts migration.MigrateOptions, migrateTimeout time.Duration) (string, error) {
+	wikiPath := repo_model.WikiPath(u.Name, opts.RepoName)
+	wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr)
+	if wikiRemotePath == "" {
+		return "", nil
+	}
+
+	if err := util.RemoveAll(wikiPath); err != nil {
+		return "", fmt.Errorf("failed to remove existing wiki dir %q, err: %w", wikiPath, err)
+	}
+
+	cleanIncompleteWikiPath := func() {
+		if err := util.RemoveAll(wikiPath); err != nil {
+			log.Error("Failed to remove incomplete wiki dir %q, err: %v", wikiPath, err)
+		}
+	}
+	if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
+		Mirror:        true,
+		Quiet:         true,
+		Timeout:       migrateTimeout,
+		SkipTLSVerify: setting.Migrations.SkipTLSVerify,
+	}); err != nil {
+		log.Error("Clone wiki failed, err: %v", err)
+		cleanIncompleteWikiPath()
+		return "", err
+	}
+
+	if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
+		cleanIncompleteWikiPath()
+		return "", err
+	}
+
+	defaultBranch, err := git.GetDefaultBranch(ctx, wikiPath)
+	if err != nil {
+		cleanIncompleteWikiPath()
+		return "", fmt.Errorf("failed to get wiki repo default branch for %q, err: %w", wikiPath, err)
+	}
+
+	return defaultBranch, nil
+}
+
+// MigrateRepositoryGitData starts migrating git related data after created migrating repository
+func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
+	repo *repo_model.Repository, opts migration.MigrateOptions,
+	httpTransport *http.Transport,
+) (*repo_model.Repository, error) {
+	repoPath := repo_model.RepoPath(u.Name, opts.RepoName)
+
+	if u.IsOrganization() {
+		t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
+		if err != nil {
+			return nil, err
+		}
+		repo.NumWatches = t.NumMembers
+	} else {
+		repo.NumWatches = 1
+	}
+
+	migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second
+
+	if err := util.RemoveAll(repoPath); err != nil {
+		return repo, fmt.Errorf("failed to remove existing repo dir %q, err: %w", repoPath, err)
+	}
+
+	if err := git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{
+		Mirror:        true,
+		Quiet:         true,
+		Timeout:       migrateTimeout,
+		SkipTLSVerify: setting.Migrations.SkipTLSVerify,
+	}); err != nil {
+		if errors.Is(err, context.DeadlineExceeded) {
+			return repo, fmt.Errorf("clone timed out, consider increasing [git.timeout] MIGRATE in app.ini, underlying err: %w", err)
+		}
+		return repo, fmt.Errorf("clone error: %w", err)
+	}
+
+	if err := git.WriteCommitGraph(ctx, repoPath); err != nil {
+		return repo, err
+	}
+
+	if opts.Wiki {
+		defaultWikiBranch, err := cloneWiki(ctx, u, opts, migrateTimeout)
+		if err != nil {
+			return repo, fmt.Errorf("clone wiki error: %w", err)
+		}
+		repo.DefaultWikiBranch = defaultWikiBranch
+	}
+
+	if repo.OwnerID == u.ID {
+		repo.Owner = u
+	}
+
+	if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
+		return repo, fmt.Errorf("checkDaemonExportOK: %w", err)
+	}
+
+	if stdout, _, err := git.NewCommand(ctx, "update-server-info").
+		SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)).
+		RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
+		log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
+		return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err)
+	}
+
+	gitRepo, err := git.OpenRepository(ctx, repoPath)
+	if err != nil {
+		return repo, fmt.Errorf("OpenRepository: %w", err)
+	}
+	defer gitRepo.Close()
+
+	repo.IsEmpty, err = gitRepo.IsEmpty()
+	if err != nil {
+		return repo, fmt.Errorf("git.IsEmpty: %w", err)
+	}
+
+	if !repo.IsEmpty {
+		if len(repo.DefaultBranch) == 0 {
+			// Try to get HEAD branch and set it as default branch.
+			headBranch, err := gitRepo.GetHEADBranch()
+			if err != nil {
+				return repo, fmt.Errorf("GetHEADBranch: %w", err)
+			}
+			if headBranch != nil {
+				repo.DefaultBranch = headBranch.Name
+			}
+		}
+
+		if _, err := repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil {
+			return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err)
+		}
+
+		if !opts.Releases {
+			// note: this will greatly improve release (tag) sync
+			// for pull-mirrors with many tags
+			repo.IsMirror = opts.Mirror
+			if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
+				log.Error("Failed to synchronize tags to releases for repository: %v", err)
+			}
+		}
+
+		if opts.LFS {
+			endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint)
+			lfsClient := lfs.NewClient(endpoint, httpTransport)
+			if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil {
+				log.Error("Failed to store missing LFS objects for repository: %v", err)
+			}
+		}
+	}
+
+	ctx, committer, err := db.TxContext(ctx)
+	if err != nil {
+		return nil, err
+	}
+	defer committer.Close()
+
+	if opts.Mirror {
+		remoteAddress, err := util.SanitizeURL(opts.CloneAddr)
+		if err != nil {
+			return repo, err
+		}
+		mirrorModel := repo_model.Mirror{
+			RepoID:         repo.ID,
+			Interval:       setting.Mirror.DefaultInterval,
+			EnablePrune:    true,
+			NextUpdateUnix: timeutil.TimeStampNow().AddDuration(setting.Mirror.DefaultInterval),
+			LFS:            opts.LFS,
+			RemoteAddress:  remoteAddress,
+		}
+		if opts.LFS {
+			mirrorModel.LFSEndpoint = opts.LFSEndpoint
+		}
+
+		if opts.MirrorInterval != "" {
+			parsedInterval, err := time.ParseDuration(opts.MirrorInterval)
+			if err != nil {
+				log.Error("Failed to set Interval: %v", err)
+				return repo, err
+			}
+			if parsedInterval == 0 {
+				mirrorModel.Interval = 0
+				mirrorModel.NextUpdateUnix = 0
+			} else if parsedInterval < setting.Mirror.MinInterval {
+				err := fmt.Errorf("interval %s is set below Minimum Interval of %s", parsedInterval, setting.Mirror.MinInterval)
+				log.Error("Interval: %s is too frequent", opts.MirrorInterval)
+				return repo, err
+			} else {
+				mirrorModel.Interval = parsedInterval
+				mirrorModel.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(parsedInterval)
+			}
+		}
+
+		if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil {
+			return repo, fmt.Errorf("InsertOne: %w", err)
+		}
+
+		repo.IsMirror = true
+		if err = UpdateRepository(ctx, repo, false); err != nil {
+			return nil, err
+		}
+
+		// this is necessary for sync local tags from remote
+		configName := fmt.Sprintf("remote.%s.fetch", mirrorModel.GetRemoteName())
+		if stdout, _, err := git.NewCommand(ctx, "config").
+			AddOptionValues("--add", configName, `+refs/tags/*:refs/tags/*`).
+			RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
+			log.Error("MigrateRepositoryGitData(git config --add <remote> +refs/tags/*:refs/tags/*) in %v: Stdout: %s\nError: %v", repo, stdout, err)
+			return repo, fmt.Errorf("error in MigrateRepositoryGitData(git config --add <remote> +refs/tags/*:refs/tags/*): %w", err)
+		}
+	} else {
+		if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
+			log.Error("Failed to update size for repository: %v", err)
+		}
+		if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil {
+			return nil, err
+		}
+	}
+
+	return repo, committer.Commit()
+}
+
+// cleanUpMigrateGitConfig removes mirror info which prevents "push --all".
+// This also removes possible user credentials.
+func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error {
+	cmd := git.NewCommand(ctx, "remote", "rm", "origin")
+	// if the origin does not exist
+	_, stderr, err := cmd.RunStdString(&git.RunOpts{
+		Dir: repoPath,
+	})
+	if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") {
+		return err
+	}
+	return nil
+}
+
+// CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors.
+func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) {
+	repoPath := repo.RepoPath()
+	if err := gitrepo.CreateDelegateHooks(ctx, repo, false); err != nil {
+		return repo, fmt.Errorf("createDelegateHooks: %w", err)
+	}
+	if repo.HasWiki() {
+		if err := gitrepo.CreateDelegateHooks(ctx, repo, true); err != nil {
+			return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err)
+		}
+	}
+
+	_, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
+	if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
+		return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err)
+	}
+
+	if repo.HasWiki() {
+		if err := cleanUpMigrateGitConfig(ctx, repo.WikiPath()); err != nil {
+			return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err)
+		}
+	}
+
+	return repo, UpdateRepository(ctx, repo, false)
+}
diff --git a/services/repository/push.go b/services/repository/push.go
index bedcf6f252..39843249a5 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -11,7 +11,6 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models/db"
-	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/cache"
@@ -93,11 +92,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 	}
 	defer gitRepo.Close()
 
-	objectFormat, err := gitRepo.GetObjectFormat()
-	if err != nil {
-		return fmt.Errorf("unknown repository ObjectFormat [%s]: %w", repo.FullName(), err)
-	}
-
 	if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
 		return fmt.Errorf("Failed to update size for repository: %v", err)
 	}
@@ -105,6 +99,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 	addTags := make([]string, 0, len(optsList))
 	delTags := make([]string, 0, len(optsList))
 	var pusher *user_model.User
+	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
 
 	for _, opts := range optsList {
 		log.Trace("pushUpdates: %-v %s %s %s", repo, opts.OldCommitID, opts.NewCommitID, opts.RefFullName)
@@ -187,7 +182,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 						repo.DefaultBranch = refName
 						repo.IsEmpty = false
 						if repo.DefaultBranch != setting.Repository.DefaultBranch {
-							if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+							if err := gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
 								if !git.IsErrUnsupportedVersion(err) {
 									return err
 								}
@@ -225,6 +220,11 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 					}
 				}
 
+				// delete cache for divergence
+				if err := DelDivergenceFromCache(repo.ID, branch); err != nil {
+					log.Error("DelDivergenceFromCache: %v", err)
+				}
+
 				commits := repo_module.GitToPushCommits(l)
 				commits.HeadCommit = repo_module.CommitToPushCommit(newCommit)
 
@@ -263,10 +263,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 					commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
 				}
 
-				if err = syncBranchToDB(ctx, repo.ID, opts.PusherID, branch, newCommit); err != nil {
-					return fmt.Errorf("git_model.UpdateBranch %s:%s failed: %v", repo.FullName(), branch, err)
-				}
-
 				notify_service.PushCommits(ctx, pusher, repo, opts, commits)
 
 				// Cache for big repository
@@ -279,10 +275,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 					// close all related pulls
 					log.Error("close related pull request failed: %v", err)
 				}
-
-				if err := git_model.AddDeletedBranch(ctx, repo.ID, branch, pusher.ID); err != nil {
-					return fmt.Errorf("AddDeletedBranch %s:%s failed: %v", repo.FullName(), branch, err)
-				}
 			}
 
 			// Even if user delete a branch on a repository which he didn't watch, he will be watch that.
@@ -321,14 +313,9 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
 		return nil
 	}
 
-	lowerTags := make([]string, 0, len(tags))
-	for _, tag := range tags {
-		lowerTags = append(lowerTags, strings.ToLower(tag))
-	}
-
 	releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
 		RepoID:   repo.ID,
-		TagNames: lowerTags,
+		TagNames: tags,
 	})
 	if err != nil {
 		return fmt.Errorf("db.Find[repo_model.Release]: %w", err)
@@ -338,6 +325,11 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
 		relMap[rel.LowerTagName] = rel
 	}
 
+	lowerTags := make([]string, 0, len(tags))
+	for _, tag := range tags {
+		lowerTags = append(lowerTags, strings.ToLower(tag))
+	}
+
 	newReleases := make([]*repo_model.Release, 0, len(lowerTags)-len(relMap))
 
 	emailToUser := make(map[string]*user_model.User)
diff --git a/services/repository/template.go b/services/repository/template.go
index 06cf05026f..36a680c8e2 100644
--- a/services/repository/template.go
+++ b/services/repository/template.go
@@ -11,7 +11,6 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
-	repo_module "code.gitea.io/gitea/modules/repository"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
 
@@ -63,7 +62,7 @@ func GenerateProtectedBranch(ctx context.Context, templateRepo, generateRepo *re
 }
 
 // GenerateRepository generates a repository from a template
-func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts repo_module.GenerateRepoOptions) (_ *repo_model.Repository, err error) {
+func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) {
 	if !doer.IsAdmin && !owner.CanCreateRepo() {
 		return nil, repo_model.ErrReachLimitOfRepo{
 			Limit: owner.MaxRepoCreation,
@@ -72,14 +71,14 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
 
 	var generateRepo *repo_model.Repository
 	if err = db.WithTx(ctx, func(ctx context.Context) error {
-		generateRepo, err = repo_module.GenerateRepository(ctx, doer, owner, templateRepo, opts)
+		generateRepo, err = generateRepository(ctx, doer, owner, templateRepo, opts)
 		if err != nil {
 			return err
 		}
 
 		// Git Content
 		if opts.GitContent && !templateRepo.IsEmpty {
-			if err = repo_module.GenerateGitContent(ctx, templateRepo, generateRepo); err != nil {
+			if err = GenerateGitContent(ctx, templateRepo, generateRepo); err != nil {
 				return err
 			}
 		}
diff --git a/services/repository/transfer.go b/services/repository/transfer.go
index b6d030b850..c929ad03fe 100644
--- a/services/repository/transfer.go
+++ b/services/repository/transfer.go
@@ -140,9 +140,9 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
 	}
 
 	// Remove redundant collaborators.
-	collaborators, err := repo_model.GetCollaborators(ctx, repo.ID, db.ListOptions{})
+	collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
 	if err != nil {
-		return fmt.Errorf("getCollaborators: %w", err)
+		return fmt.Errorf("GetCollaborators: %w", err)
 	}
 
 	// Dummy object.
@@ -202,13 +202,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
 		return fmt.Errorf("decrease old owner repository count: %w", err)
 	}
 
-	if err := repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
+	if err := repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
 		return fmt.Errorf("watchRepo: %w", err)
 	}
 
 	// Remove watch for organization.
 	if oldOwner.IsOrganization() {
-		if err := repo_model.WatchRepo(ctx, oldOwner.ID, repo.ID, false); err != nil {
+		if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil {
 			return fmt.Errorf("watchRepo [false]: %w", err)
 		}
 	}
@@ -365,6 +365,10 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
 		return TransferOwnership(ctx, doer, newOwner, repo, teams)
 	}
 
+	if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) {
+		return user_model.ErrBlockedUser
+	}
+
 	// If new owner is an org and user can create repos he can transfer directly too
 	if newOwner.IsOrganization() {
 		allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID)
diff --git a/services/user/block.go b/services/user/block.go
new file mode 100644
index 0000000000..0b3b618aae
--- /dev/null
+++ b/services/user/block.go
@@ -0,0 +1,308 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	org_model "code.gitea.io/gitea/models/organization"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	repo_service "code.gitea.io/gitea/services/repository"
+)
+
+func CanBlockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
+	if blocker.ID == blockee.ID {
+		return false
+	}
+	if doer.ID == blockee.ID {
+		return false
+	}
+
+	if blockee.IsOrganization() {
+		return false
+	}
+
+	if user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
+		return false
+	}
+
+	if blocker.IsOrganization() {
+		org := org_model.OrgFromUser(blocker)
+		if isMember, _ := org.IsOrgMember(ctx, blockee.ID); isMember {
+			return false
+		}
+		if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
+			return false
+		}
+	} else if !doer.IsAdmin && doer.ID != blocker.ID {
+		return false
+	}
+
+	return true
+}
+
+func CanUnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
+	if doer.ID == blockee.ID {
+		return false
+	}
+
+	if !user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
+		return false
+	}
+
+	if blocker.IsOrganization() {
+		org := org_model.OrgFromUser(blocker)
+		if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
+			return false
+		}
+	} else if !doer.IsAdmin && doer.ID != blocker.ID {
+		return false
+	}
+
+	return true
+}
+
+func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, note string) error {
+	if blockee.IsOrganization() {
+		return user_model.ErrBlockOrganization
+	}
+
+	if !CanBlockUser(ctx, doer, blocker, blockee) {
+		return user_model.ErrCanNotBlock
+	}
+
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		// unfollow each other
+		if err := user_model.UnfollowUser(ctx, blocker.ID, blockee.ID); err != nil {
+			return err
+		}
+		if err := user_model.UnfollowUser(ctx, blockee.ID, blocker.ID); err != nil {
+			return err
+		}
+
+		// unstar each other
+		if err := unstarRepos(ctx, blocker, blockee); err != nil {
+			return err
+		}
+		if err := unstarRepos(ctx, blockee, blocker); err != nil {
+			return err
+		}
+
+		// unwatch each others repositories
+		if err := unwatchRepos(ctx, blocker, blockee); err != nil {
+			return err
+		}
+		if err := unwatchRepos(ctx, blockee, blocker); err != nil {
+			return err
+		}
+
+		// unassign each other from issues
+		if err := unassignIssues(ctx, blocker, blockee); err != nil {
+			return err
+		}
+		if err := unassignIssues(ctx, blockee, blocker); err != nil {
+			return err
+		}
+
+		// remove each other from repository collaborations
+		if err := removeCollaborations(ctx, blocker, blockee); err != nil {
+			return err
+		}
+		if err := removeCollaborations(ctx, blockee, blocker); err != nil {
+			return err
+		}
+
+		// cancel each other repository transfers
+		if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil {
+			return err
+		}
+		if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil {
+			return err
+		}
+
+		return db.Insert(ctx, &user_model.Blocking{
+			BlockerID: blocker.ID,
+			BlockeeID: blockee.ID,
+			Note:      note,
+		})
+	})
+}
+
+func unstarRepos(ctx context.Context, starrer, repoOwner *user_model.User) error {
+	opts := &repo_model.StarredReposOptions{
+		ListOptions: db.ListOptions{
+			Page:     1,
+			PageSize: 25,
+		},
+		StarrerID:   starrer.ID,
+		RepoOwnerID: repoOwner.ID,
+	}
+
+	for {
+		repos, err := repo_model.GetStarredRepos(ctx, opts)
+		if err != nil {
+			return err
+		}
+
+		if len(repos) == 0 {
+			return nil
+		}
+
+		for _, repo := range repos {
+			if err := repo_model.StarRepo(ctx, starrer, repo, false); err != nil {
+				return err
+			}
+		}
+
+		opts.Page++
+	}
+}
+
+func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) error {
+	opts := &repo_model.WatchedReposOptions{
+		ListOptions: db.ListOptions{
+			Page:     1,
+			PageSize: 25,
+		},
+		WatcherID:   watcher.ID,
+		RepoOwnerID: repoOwner.ID,
+	}
+
+	for {
+		repos, _, err := repo_model.GetWatchedRepos(ctx, opts)
+		if err != nil {
+			return err
+		}
+
+		if len(repos) == 0 {
+			return nil
+		}
+
+		for _, repo := range repos {
+			if err := repo_model.WatchRepo(ctx, watcher, repo, false); err != nil {
+				return err
+			}
+		}
+
+		opts.Page++
+	}
+}
+
+func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error {
+	transfers, err := models.GetPendingRepositoryTransfers(ctx, &models.PendingRepositoryTransferOptions{
+		SenderID:    sender.ID,
+		RecipientID: recipient.ID,
+	})
+	if err != nil {
+		return err
+	}
+
+	for _, transfer := range transfers {
+		repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID)
+		if err != nil {
+			return err
+		}
+
+		if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func unassignIssues(ctx context.Context, assignee, repoOwner *user_model.User) error {
+	opts := &issues_model.AssignedIssuesOptions{
+		ListOptions: db.ListOptions{
+			Page:     1,
+			PageSize: 25,
+		},
+		AssigneeID:  assignee.ID,
+		RepoOwnerID: repoOwner.ID,
+	}
+
+	for {
+		issues, _, err := issues_model.GetAssignedIssues(ctx, opts)
+		if err != nil {
+			return err
+		}
+
+		if len(issues) == 0 {
+			return nil
+		}
+
+		for _, issue := range issues {
+			if err := issue.LoadAssignees(ctx); err != nil {
+				return err
+			}
+
+			if _, _, err := issues_model.ToggleIssueAssignee(ctx, issue, assignee, assignee.ID); err != nil {
+				return err
+			}
+		}
+
+		opts.Page++
+	}
+}
+
+func removeCollaborations(ctx context.Context, repoOwner, collaborator *user_model.User) error {
+	opts := &repo_model.FindCollaborationOptions{
+		ListOptions: db.ListOptions{
+			Page:     1,
+			PageSize: 25,
+		},
+		CollaboratorID: collaborator.ID,
+		RepoOwnerID:    repoOwner.ID,
+	}
+
+	for {
+		collaborations, _, err := repo_model.GetCollaborators(ctx, opts)
+		if err != nil {
+			return err
+		}
+
+		if len(collaborations) == 0 {
+			return nil
+		}
+
+		for _, collaboration := range collaborations {
+			repo, err := repo_model.GetRepositoryByID(ctx, collaboration.Collaboration.RepoID)
+			if err != nil {
+				return err
+			}
+
+			if err := repo_service.DeleteCollaboration(ctx, repo, collaborator); err != nil {
+				return err
+			}
+		}
+
+		opts.Page++
+	}
+}
+
+func UnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) error {
+	if blockee.IsOrganization() {
+		return user_model.ErrBlockOrganization
+	}
+
+	if !CanUnblockUser(ctx, doer, blocker, blockee) {
+		return user_model.ErrCanNotUnblock
+	}
+
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
+		if err != nil {
+			return err
+		}
+		if block != nil {
+			_, err = db.DeleteByID[user_model.Blocking](ctx, block.ID)
+			return err
+		}
+		return nil
+	})
+}
diff --git a/services/user/block_test.go b/services/user/block_test.go
new file mode 100644
index 0000000000..aec3e03cf3
--- /dev/null
+++ b/services/user/block_test.go
@@ -0,0 +1,66 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCanBlockUser(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+	user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
+	org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
+
+	// Doer can't self block
+	assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user1))
+	// Blocker can't be blockee
+	assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user2))
+	// Can't block already blocked user
+	assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user29))
+	// Blockee can't be an organization
+	assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, org3))
+	// Doer must be blocker or admin
+	assert.False(t, CanBlockUser(db.DefaultContext, user2, user4, user29))
+	// Organization can't block a member
+	assert.False(t, CanBlockUser(db.DefaultContext, user1, org3, user4))
+	// Doer must be organization owner or admin if blocker is an organization
+	assert.False(t, CanBlockUser(db.DefaultContext, user4, org3, user2))
+
+	assert.True(t, CanBlockUser(db.DefaultContext, user1, user2, user4))
+	assert.True(t, CanBlockUser(db.DefaultContext, user2, user2, user4))
+	assert.True(t, CanBlockUser(db.DefaultContext, user2, org3, user29))
+}
+
+func TestCanUnblockUser(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	user28 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
+	user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
+	org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
+
+	// Doer can't self unblock
+	assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user1))
+	// Can't unblock not blocked user
+	assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user28))
+	// Doer must be blocker or admin
+	assert.False(t, CanUnblockUser(db.DefaultContext, user28, user2, user29))
+	// Doer must be organization owner or admin if blocker is an organization
+	assert.False(t, CanUnblockUser(db.DefaultContext, user2, org17, user28))
+
+	assert.True(t, CanUnblockUser(db.DefaultContext, user1, user2, user29))
+	assert.True(t, CanUnblockUser(db.DefaultContext, user2, user2, user29))
+	assert.True(t, CanUnblockUser(db.DefaultContext, user1, org17, user28))
+}
diff --git a/services/user/delete.go b/services/user/delete.go
index 000910319a..889da3eb67 100644
--- a/services/user/delete.go
+++ b/services/user/delete.go
@@ -92,6 +92,9 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
 		&pull_model.ReviewState{UserID: u.ID},
 		&user_model.Redirect{RedirectUserID: u.ID},
 		&actions_model.ActionRunner{OwnerID: u.ID},
+		&user_model.Blocking{BlockerID: u.ID},
+		&user_model.Blocking{BlockeeID: u.ID},
+		&actions_model.ActionRunnerToken{OwnerID: u.ID},
 	); err != nil {
 		return fmt.Errorf("deleteBeans: %w", err)
 	}
diff --git a/services/user/email.go b/services/user/email.go
index 07e19bc688..5c0de708e9 100644
--- a/services/user/email.go
+++ b/services/user/email.go
@@ -14,12 +14,13 @@ import (
 	"code.gitea.io/gitea/modules/util"
 )
 
-func AddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
+// AdminAddOrSetPrimaryEmailAddress is used by admins to add or set a user's primary email address
+func AdminAddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
 	if strings.EqualFold(u.Email, emailStr) {
 		return nil
 	}
 
-	if err := user_model.ValidateEmail(emailStr); err != nil {
+	if err := user_model.ValidateEmailForAdmin(emailStr); err != nil {
 		return err
 	}
 
diff --git a/services/user/email_test.go b/services/user/email_test.go
index 8f419b69f9..b40f86b6a6 100644
--- a/services/user/email_test.go
+++ b/services/user/email_test.go
@@ -10,11 +10,13 @@ import (
 	organization_model "code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/setting"
 
+	"github.com/gobwas/glob"
 	"github.com/stretchr/testify/assert"
 )
 
-func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
+func TestAdminAddOrSetPrimaryEmailAddress(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 27})
@@ -28,7 +30,7 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
 	assert.NotEqual(t, "new-primary@example.com", primary.Email)
 	assert.Equal(t, user.Email, primary.Email)
 
-	assert.NoError(t, AddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary@example.com"))
+	assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary@example.com"))
 
 	primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
 	assert.NoError(t, err)
@@ -39,7 +41,19 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, emails, 2)
 
-	assert.NoError(t, AddOrSetPrimaryEmailAddress(db.DefaultContext, user, "user27@example.com"))
+	setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("example.org")}
+	defer func() {
+		setting.Service.EmailDomainAllowList = []glob.Glob{}
+	}()
+
+	assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary2@example2.com"))
+
+	primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
+	assert.NoError(t, err)
+	assert.Equal(t, "new-primary2@example2.com", primary.Email)
+	assert.Equal(t, user.Email, primary.Email)
+
+	assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "user27@example.com"))
 
 	primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
 	assert.NoError(t, err)
@@ -48,7 +62,7 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
 
 	emails, err = user_model.GetEmailAddresses(db.DefaultContext, user.ID)
 	assert.NoError(t, err)
-	assert.Len(t, emails, 2)
+	assert.Len(t, emails, 3)
 }
 
 func TestReplacePrimaryEmailAddress(t *testing.T) {
diff --git a/services/user/user.go b/services/user/user.go
index 8ffd1e98a9..bb4c7dbee7 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -11,7 +11,6 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
-	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	packages_model "code.gitea.io/gitea/models/packages"
@@ -25,6 +24,7 @@ import (
 	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/agit"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	org_service "code.gitea.io/gitea/services/org"
 	"code.gitea.io/gitea/services/packages"
 	container_service "code.gitea.io/gitea/services/packages/container"
@@ -127,7 +127,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
 		return fmt.Errorf("%s is an organization not a user", u.Name)
 	}
 
-	if user_model.IsLastAdminUser(ctx, u) {
+	if u.IsActive && user_model.IsLastAdminUser(ctx, u) {
 		return models.ErrDeleteLastAdminUser{UID: u.ID}
 	}
 
@@ -189,7 +189,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
 				break
 			}
 			for _, org := range orgs {
-				if err := models.RemoveOrgUser(ctx, org.ID, u.ID); err != nil {
+				if err := models.RemoveOrgUser(ctx, org, u); err != nil {
 					if organization.IsErrLastOrgOwner(err) {
 						err = org_service.DeleteOrganization(ctx, org, true)
 						if err != nil {
@@ -251,59 +251,54 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
 	if err := committer.Commit(); err != nil {
 		return err
 	}
-	committer.Close()
+	_ = committer.Close()
 
-	if err = asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+	if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
 		return err
 	}
-	if err = asymkey_model.RewriteAllPrincipalKeys(ctx); err != nil {
+	if err = asymkey_service.RewriteAllPrincipalKeys(ctx); err != nil {
 		return err
 	}
 
-	// Note: There are something just cannot be roll back,
-	//	so just keep error logs of those operations.
+	// Note: There are something just cannot be roll back, so just keep error logs of those operations.
 	path := user_model.UserPath(u.Name)
-	if err := util.RemoveAll(path); err != nil {
-		err = fmt.Errorf("Failed to RemoveAll %s: %w", path, err)
+	if err = util.RemoveAll(path); err != nil {
+		err = fmt.Errorf("failed to RemoveAll %s: %w", path, err)
 		_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
-		return err
 	}
 
 	if u.Avatar != "" {
 		avatarPath := u.CustomAvatarRelativePath()
-		if err := storage.Avatars.Delete(avatarPath); err != nil {
-			err = fmt.Errorf("Failed to remove %s: %w", avatarPath, err)
+		if err = storage.Avatars.Delete(avatarPath); err != nil {
+			err = fmt.Errorf("failed to remove %s: %w", avatarPath, err)
 			_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
-			return err
 		}
 	}
 
 	return nil
 }
 
-// DeleteInactiveUsers deletes all inactive users and email addresses.
+// DeleteInactiveUsers deletes all inactive users and their email addresses.
 func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
-	users, err := user_model.GetInactiveUsers(ctx, olderThan)
+	inactiveUsers, err := user_model.GetInactiveUsers(ctx, olderThan)
 	if err != nil {
 		return err
 	}
 
 	// FIXME: should only update authorized_keys file once after all deletions.
-	for _, u := range users {
-		select {
-		case <-ctx.Done():
-			return db.ErrCancelledf("Before delete inactive user %s", u.Name)
-		default:
-		}
-		if err := DeleteUser(ctx, u, false); err != nil {
-			// Ignore users that were set inactive by admin.
-			if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) ||
-				models.IsErrUserOwnPackages(err) || models.IsErrDeleteLastAdminUser(err) {
+	for _, u := range inactiveUsers {
+		if err = DeleteUser(ctx, u, false); err != nil {
+			// Ignore inactive users that were ever active but then were set inactive by admin
+			if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) {
 				continue
 			}
-			return err
+			select {
+			case <-ctx.Done():
+				return db.ErrCancelledf("when deleting inactive user %q", u.Name)
+			default:
+				return err
+			}
 		}
 	}
-
-	return user_model.DeleteInactiveEmailAddresses(ctx)
+	return nil // TODO: there could be still inactive users left, and the number would increase gradually
 }
diff --git a/services/user/user_test.go b/services/user/user_test.go
index 2ebcded925..bd6019a14f 100644
--- a/services/user/user_test.go
+++ b/services/user/user_test.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"strings"
 	"testing"
+	"time"
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/auth"
@@ -16,6 +17,7 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/timeutil"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -41,7 +43,8 @@ func TestDeleteUser(t *testing.T) {
 		orgUsers := make([]*organization.OrgUser, 0, 10)
 		assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &organization.OrgUser{UID: userID}))
 		for _, orgUser := range orgUsers {
-			if err := models.RemoveOrgUser(db.DefaultContext, orgUser.OrgID, orgUser.UID); err != nil {
+			org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: orgUser.OrgID})
+			if err := models.RemoveOrgUser(db.DefaultContext, org, user); err != nil {
 				assert.True(t, organization.IsErrLastOrgOwner(err))
 				return
 			}
@@ -184,3 +187,26 @@ func TestCreateUser_Issue5882(t *testing.T) {
 		assert.NoError(t, DeleteUser(db.DefaultContext, v.user, false))
 	}
 }
+
+func TestDeleteInactiveUsers(t *testing.T) {
+	addUser := func(name, email string, createdUnix timeutil.TimeStamp, active bool) {
+		inactiveUser := &user_model.User{Name: name, LowerName: strings.ToLower(name), Email: email, CreatedUnix: createdUnix, IsActive: active}
+		_, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(inactiveUser)
+		assert.NoError(t, err)
+		inactiveUserEmail := &user_model.EmailAddress{UID: inactiveUser.ID, IsPrimary: true, Email: email, LowerEmail: strings.ToLower(email), IsActivated: active}
+		err = db.Insert(db.DefaultContext, inactiveUserEmail)
+		assert.NoError(t, err)
+	}
+	addUser("user-inactive-10", "user-inactive-10@test.com", timeutil.TimeStampNow().Add(-600), false)
+	addUser("user-inactive-5", "user-inactive-5@test.com", timeutil.TimeStampNow().Add(-300), false)
+	addUser("user-active-10", "user-active-10@test.com", timeutil.TimeStampNow().Add(-600), true)
+	addUser("user-active-5", "user-active-5@test.com", timeutil.TimeStampNow().Add(-300), true)
+	unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-inactive-10"})
+	unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user-inactive-10@test.com"})
+	assert.NoError(t, DeleteInactiveUsers(db.DefaultContext, 8*time.Minute))
+	unittest.AssertNotExistsBean(t, &user_model.User{Name: "user-inactive-10"})
+	unittest.AssertNotExistsBean(t, &user_model.EmailAddress{Email: "user-inactive-10@test.com"})
+	unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-inactive-5"})
+	unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-active-10"})
+	unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-active-5"})
+}
diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go
index 8f728d3aa6..b2c0a73784 100644
--- a/services/webhook/deliver.go
+++ b/services/webhook/deliver.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"crypto/hmac"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/tls"
 	"encoding/hex"
 	"fmt"
@@ -29,39 +30,19 @@ import (
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
 	"github.com/gobwas/glob"
-	"github.com/minio/sha256-simd"
 )
 
-// Deliver deliver hook task
-func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
-	w, err := webhook_model.GetWebhookByID(ctx, t.HookID)
-	if err != nil {
-		return err
-	}
-
-	defer func() {
-		err := recover()
-		if err == nil {
-			return
-		}
-		// There was a panic whilst delivering a hook...
-		log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2))
-	}()
-
-	t.IsDelivered = true
-
-	var req *http.Request
-
+func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) {
 	switch w.HTTPMethod {
 	case "":
-		log.Info("HTTP Method for webhook %s empty, setting to POST as default", w.URL)
+		log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID)
 		fallthrough
 	case http.MethodPost:
 		switch w.ContentType {
 		case webhook_model.ContentTypeJSON:
 			req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent))
 			if err != nil {
-				return err
+				return nil, nil, err
 			}
 
 			req.Header.Set("Content-Type", "application/json")
@@ -72,50 +53,58 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
 
 			req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode()))
 			if err != nil {
-				return err
+				return nil, nil, err
 			}
 
 			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+		default:
+			return nil, nil, fmt.Errorf("invalid content type: %v", w.ContentType)
 		}
 	case http.MethodGet:
 		u, err := url.Parse(w.URL)
 		if err != nil {
-			return fmt.Errorf("unable to deliver webhook task[%d] as cannot parse webhook url %s: %w", t.ID, w.URL, err)
+			return nil, nil, fmt.Errorf("invalid URL: %w", err)
 		}
 		vals := u.Query()
 		vals["payload"] = []string{t.PayloadContent}
 		u.RawQuery = vals.Encode()
 		req, err = http.NewRequest("GET", u.String(), nil)
 		if err != nil {
-			return fmt.Errorf("unable to deliver webhook task[%d] as unable to create HTTP request for webhook url %s: %w", t.ID, w.URL, err)
+			return nil, nil, err
 		}
 	case http.MethodPut:
 		switch w.Type {
-		case webhook_module.MATRIX:
+		case webhook_module.MATRIX: // used when t.Version == 1
 			txnID, err := getMatrixTxnID([]byte(t.PayloadContent))
 			if err != nil {
-				return err
+				return nil, nil, err
 			}
 			url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
 			req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent))
 			if err != nil {
-				return fmt.Errorf("unable to deliver webhook task[%d] as cannot create matrix request for webhook url %s: %w", t.ID, w.URL, err)
+				return nil, nil, err
 			}
 		default:
-			return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod)
+			return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
 		}
 	default:
-		return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod)
+		return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
 	}
 
+	body = []byte(t.PayloadContent)
+	return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
+}
+
+func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error {
 	var signatureSHA1 string
 	var signatureSHA256 string
-	if len(w.Secret) > 0 {
-		sig1 := hmac.New(sha1.New, []byte(w.Secret))
-		sig256 := hmac.New(sha256.New, []byte(w.Secret))
-		_, err = io.MultiWriter(sig1, sig256).Write([]byte(t.PayloadContent))
+	if len(secret) > 0 {
+		sig1 := hmac.New(sha1.New, secret)
+		sig256 := hmac.New(sha256.New, secret)
+		_, err := io.MultiWriter(sig1, sig256).Write(payloadContent)
 		if err != nil {
-			log.Error("prepareWebhooks.sigWrite: %v", err)
+			// this error should never happen, since the hashes are writing to []byte and always return a nil error.
+			return fmt.Errorf("prepareWebhooks.sigWrite: %w", err)
 		}
 		signatureSHA1 = hex.EncodeToString(sig1.Sum(nil))
 		signatureSHA256 = hex.EncodeToString(sig256.Sum(nil))
@@ -136,15 +125,36 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
 	req.Header["X-GitHub-Delivery"] = []string{t.UUID}
 	req.Header["X-GitHub-Event"] = []string{event}
 	req.Header["X-GitHub-Event-Type"] = []string{eventType}
+	return nil
+}
 
-	// Add Authorization Header
-	authorization, err := w.HeaderAuthorization()
+// Deliver creates the [http.Request] (depending on the webhook type), sends it
+// and records the status and response.
+func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
+	w, err := webhook_model.GetWebhookByID(ctx, t.HookID)
 	if err != nil {
-		log.Error("Webhook could not get Authorization header [%d]: %v", w.ID, err)
 		return err
 	}
-	if authorization != "" {
-		req.Header["Authorization"] = []string{authorization}
+
+	defer func() {
+		err := recover()
+		if err == nil {
+			return
+		}
+		// There was a panic whilst delivering a hook...
+		log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2))
+	}()
+
+	t.IsDelivered = true
+
+	newRequest := webhookRequesters[w.Type]
+	if t.PayloadVersion == 1 || newRequest == nil {
+		newRequest = newDefaultRequest
+	}
+
+	req, body, err := newRequest(ctx, w, t)
+	if err != nil {
+		return fmt.Errorf("cannot create http request for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err)
 	}
 
 	// Record delivery information.
@@ -152,11 +162,22 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
 		URL:        req.URL.String(),
 		HTTPMethod: req.Method,
 		Headers:    map[string]string{},
+		Body:       string(body),
 	}
 	for k, vals := range req.Header {
 		t.RequestInfo.Headers[k] = strings.Join(vals, ",")
 	}
 
+	// Add Authorization Header
+	authorization, err := w.HeaderAuthorization()
+	if err != nil {
+		return fmt.Errorf("cannot get Authorization header for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err)
+	}
+	if authorization != "" {
+		req.Header.Set("Authorization", authorization)
+		t.RequestInfo.Headers["Authorization"] = "******"
+	}
+
 	t.ResponseInfo = &webhook_model.HookResponse{
 		Headers: map[string]string{},
 	}
diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go
index 72aa00478a..d0cfc1598f 100644
--- a/services/webhook/deliver_test.go
+++ b/services/webhook/deliver_test.go
@@ -5,9 +5,11 @@ package webhook
 
 import (
 	"context"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
+	"strings"
 	"testing"
 	"time"
 
@@ -16,7 +18,7 @@ import (
 	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/hostmatcher"
 	"code.gitea.io/gitea/modules/setting"
-	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
 	"github.com/stretchr/testify/assert"
@@ -105,15 +107,16 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
 	err := hook.SetHeaderAuthorization("Bearer s3cr3t-t0ken")
 	assert.NoError(t, err)
 	assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
-	db.GetEngine(db.DefaultContext).NoAutoTime().DB().Logger.ShowSQL(true)
 
-	hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush, Payloader: &api.PushPayload{}}
+	hookTask := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadVersion: 2,
+	}
 
 	hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
 	assert.NoError(t, err)
-	if !assert.NotNil(t, hookTask) {
-		return
-	}
+	assert.NotNil(t, hookTask)
 
 	assert.NoError(t, Deliver(context.Background(), hookTask))
 	select {
@@ -123,4 +126,171 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
 	}
 
 	assert.True(t, hookTask.IsSucceed)
+	assert.Equal(t, "******", hookTask.RequestInfo.Headers["Authorization"])
+}
+
+func TestWebhookDeliverHookTask(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	done := make(chan struct{}, 1)
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "PUT", r.Method)
+		switch r.URL.Path {
+		case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98":
+			// Version 1
+			assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
+			assert.Equal(t, "", r.Header.Get("Content-Type"))
+			body, err := io.ReadAll(r.Body)
+			assert.NoError(t, err)
+			assert.Equal(t, `{"data": 42}`, string(body))
+
+		case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51":
+			// Version 2
+			assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
+			assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
+			body, err := io.ReadAll(r.Body)
+			assert.NoError(t, err)
+			assert.Len(t, body, 2147)
+
+		default:
+			w.WriteHeader(404)
+			t.Fatalf("unexpected url path %s", r.URL.Path)
+			return
+		}
+		w.WriteHeader(200)
+		done <- struct{}{}
+	}))
+	t.Cleanup(s.Close)
+
+	hook := &webhook_model.Webhook{
+		RepoID:      3,
+		IsActive:    true,
+		Type:        webhook_module.MATRIX,
+		URL:         s.URL + "/webhook",
+		HTTPMethod:  "PUT",
+		ContentType: webhook_model.ContentTypeJSON,
+		Meta:        `{"message_type":0}`, // text
+	}
+	assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
+
+	t.Run("Version 1", func(t *testing.T) {
+		hookTask := &webhook_model.HookTask{
+			HookID:         hook.ID,
+			EventType:      webhook_module.HookEventPush,
+			PayloadContent: `{"data": 42}`,
+			PayloadVersion: 1,
+		}
+
+		hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask)
+		assert.NoError(t, err)
+		assert.NotNil(t, hookTask)
+
+		assert.NoError(t, Deliver(context.Background(), hookTask))
+		select {
+		case <-done:
+		case <-time.After(5 * time.Second):
+			t.Fatal("waited to long for request to happen")
+		}
+
+		assert.True(t, hookTask.IsSucceed)
+	})
+
+	t.Run("Version 2", func(t *testing.T) {
+		p := pushTestPayload()
+		data, err := p.JSONPayload()
+		assert.NoError(t, err)
+
+		hookTask := &webhook_model.HookTask{
+			HookID:         hook.ID,
+			EventType:      webhook_module.HookEventPush,
+			PayloadContent: string(data),
+			PayloadVersion: 2,
+		}
+
+		hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
+		assert.NoError(t, err)
+		assert.NotNil(t, hookTask)
+
+		assert.NoError(t, Deliver(context.Background(), hookTask))
+		select {
+		case <-done:
+		case <-time.After(5 * time.Second):
+			t.Fatal("waited to long for request to happen")
+		}
+
+		assert.True(t, hookTask.IsSucceed)
+	})
+}
+
+func TestWebhookDeliverSpecificTypes(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	type hookCase struct {
+		gotBody    chan []byte
+		httpMethod string // default to POST
+	}
+
+	cases := map[string]*hookCase{
+		webhook_module.SLACK:      {},
+		webhook_module.DISCORD:    {},
+		webhook_module.DINGTALK:   {},
+		webhook_module.TELEGRAM:   {},
+		webhook_module.MSTEAMS:    {},
+		webhook_module.FEISHU:     {},
+		webhook_module.MATRIX:     {httpMethod: "PUT"},
+		webhook_module.WECHATWORK: {},
+		webhook_module.PACKAGIST:  {},
+	}
+
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		typ := strings.Split(r.URL.Path, "/")[1] // URL: "/{webhook_type}/other-path"
+		assert.Equal(t, "application/json", r.Header.Get("Content-Type"), r.URL.Path)
+		assert.Equal(t, util.IfZero(cases[typ].httpMethod, "POST"), r.Method, "webhook test request %q", r.URL.Path)
+		body, _ := io.ReadAll(r.Body) // read request and send it back to the test by testcase's chan
+		cases[typ].gotBody <- body
+		w.WriteHeader(http.StatusNoContent)
+	}))
+	t.Cleanup(s.Close)
+
+	p := pushTestPayload()
+	data, err := p.JSONPayload()
+	assert.NoError(t, err)
+
+	for typ := range cases {
+		cases[typ].gotBody = make(chan []byte, 1)
+		t.Run(typ, func(t *testing.T) {
+			t.Parallel()
+			hook := &webhook_model.Webhook{
+				RepoID:   3,
+				IsActive: true,
+				Type:     typ,
+				URL:      s.URL + "/" + typ,
+				Meta:     "{}",
+			}
+			assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
+
+			hookTask := &webhook_model.HookTask{
+				HookID:         hook.ID,
+				EventType:      webhook_module.HookEventPush,
+				PayloadContent: string(data),
+				PayloadVersion: 2,
+			}
+
+			hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask)
+			assert.NoError(t, err)
+			assert.NotNil(t, hookTask)
+
+			assert.NoError(t, Deliver(context.Background(), hookTask))
+
+			select {
+			case gotBody := <-cases[typ].gotBody:
+				assert.NotEqual(t, string(data), string(gotBody), "request body must be different from the event payload")
+				assert.Equal(t, hookTask.RequestInfo.Body, string(gotBody), "delivered webhook payload doesn't match saved request")
+			case <-time.After(5 * time.Second):
+				t.Fatal("waited to long for request to happen")
+			}
+
+			assert.True(t, hookTask.IsSucceed)
+		})
+	}
 }
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index d615e7254f..c57d04415a 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -4,12 +4,14 @@
 package webhook
 
 import (
+	"context"
 	"fmt"
+	"net/http"
 	"net/url"
 	"strings"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -22,19 +24,8 @@ type (
 	DingtalkPayload dingtalk.Payload
 )
 
-var _ PayloadConvertor = &DingtalkPayload{}
-
-// JSONPayload Marshals the DingtalkPayload to json
-func (d *DingtalkPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(d, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
 // Create implements PayloadConvertor Create method
-func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Create(p *api.CreatePayload) (DingtalkPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -43,7 +34,7 @@ func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
 }
 
 // Delete implements PayloadConvertor Delete method
-func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Delete(p *api.DeletePayload) (DingtalkPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -52,14 +43,14 @@ func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
 }
 
 // Fork implements PayloadConvertor Fork method
-func (d *DingtalkPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Fork(p *api.ForkPayload) (DingtalkPayload, error) {
 	title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
 
 	return createDingtalkPayload(title, title, fmt.Sprintf("view forked repo %s", p.Repo.FullName), p.Repo.HTMLURL), nil
 }
 
 // Push implements PayloadConvertor Push method
-func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Push(p *api.PushPayload) (DingtalkPayload, error) {
 	var (
 		branchName = git.RefName(p.Ref).ShortName()
 		commitDesc string
@@ -100,14 +91,14 @@ func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 }
 
 // Issue implements PayloadConvertor Issue method
-func (d *DingtalkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Issue(p *api.IssuePayload) (DingtalkPayload, error) {
 	text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true)
 
 	return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view issue", p.Issue.HTMLURL), nil
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (d *DingtalkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Wiki(p *api.WikiPayload) (DingtalkPayload, error) {
 	text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
 	url := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)
 
@@ -115,27 +106,27 @@ func (d *DingtalkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (d *DingtalkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) IssueComment(p *api.IssueCommentPayload) (DingtalkPayload, error) {
 	text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)
 
 	return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+p.Comment.Body, "view issue comment", p.Comment.HTMLURL), nil
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (d *DingtalkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) PullRequest(p *api.PullRequestPayload) (DingtalkPayload, error) {
 	text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true)
 
 	return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view pull request", p.PullRequest.HTMLURL), nil
 }
 
 // Review implements PayloadConvertor Review method
-func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (dc dingtalkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DingtalkPayload, error) {
 	var text, title string
 	switch p.Action {
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return DingtalkPayload{}, err
 		}
 
 		title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -146,14 +137,14 @@ func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module
 }
 
 // Repository implements PayloadConvertor Repository method
-func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Repository(p *api.RepositoryPayload) (DingtalkPayload, error) {
 	switch p.Action {
 	case api.HookRepoCreated:
 		title := fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
 		return createDingtalkPayload(title, title, "view repository", p.Repository.HTMLURL), nil
 	case api.HookRepoDeleted:
 		title := fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
-		return &DingtalkPayload{
+		return DingtalkPayload{
 			MsgType: "text",
 			Text: struct {
 				Content string `json:"content"`
@@ -163,24 +154,24 @@ func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e
 		}, nil
 	}
 
-	return nil, nil
+	return DingtalkPayload{}, nil
 }
 
 // Release implements PayloadConvertor Release method
-func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Release(p *api.ReleasePayload) (DingtalkPayload, error) {
 	text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
 
 	return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil
 }
 
-func (d *DingtalkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, error) {
 	text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
 
 	return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
 }
 
-func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload {
-	return &DingtalkPayload{
+func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
+	return DingtalkPayload{
 		MsgType: "actionCard",
 		ActionCard: dingtalk.ActionCard{
 			Text:        strings.TrimSpace(text),
@@ -195,7 +186,10 @@ func createDingtalkPayload(title, text, singleTitle, singleURL string) *Dingtalk
 	}
 }
 
-// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload
-func GetDingtalkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
-	return convertPayloader(new(DingtalkPayload), p, event)
+type dingtalkConvertor struct{}
+
+var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{}
+
+func newDingtalkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	return newJSONRequest(dingtalkConvertor{}, w, t, true)
 }
diff --git a/services/webhook/dingtalk_test.go b/services/webhook/dingtalk_test.go
index a03fa46f14..25f47347d0 100644
--- a/services/webhook/dingtalk_test.go
+++ b/services/webhook/dingtalk_test.go
@@ -4,9 +4,12 @@
 package webhook
 
 import (
+	"context"
 	"net/url"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -24,248 +27,226 @@ func TestDingTalkPayload(t *testing.T) {
 		}
 		return ""
 	}
+	dc := dingtalkConvertor{}
 
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Create(p)
+		pl, err := dc.Create(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] branch test created", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] branch test created", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view ref test", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] branch test created", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] branch test created", pl.ActionCard.Title)
+		assert.Equal(t, "view ref test", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Delete(p)
+		pl, err := dc.Delete(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] branch test deleted", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] branch test deleted", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view ref test", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] branch test deleted", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] branch test deleted", pl.ActionCard.Title)
+		assert.Equal(t, "view ref test", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Fork(p)
+		pl, err := dc.Fork(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view forked repo test/repo", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "test/repo2 is forked to test/repo", pl.ActionCard.Text)
+		assert.Equal(t, "test/repo2 is forked to test/repo", pl.ActionCard.Title)
+		assert.Equal(t, "view forked repo test/repo", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Push(p)
+		pl, err := dc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view commits", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo:test] 2 new commits", pl.ActionCard.Title)
+		assert.Equal(t, "view commits", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(DingtalkPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := dc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\nissue body", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view issue", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\nissue body", pl.ActionCard.Text)
+		assert.Equal(t, "#2 crash", pl.ActionCard.Title)
+		assert.Equal(t, "view issue", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.ActionCard.SingleURL))
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = dc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view issue", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.ActionCard.Text)
+		assert.Equal(t, "#2 crash", pl.ActionCard.Title)
+		assert.Equal(t, "view issue", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := dc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\r\n\r\nmore info needed", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view issue comment", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\r\n\r\nmore info needed", pl.ActionCard.Text)
+		assert.Equal(t, "#2 crash", pl.ActionCard.Title)
+		assert.Equal(t, "view issue comment", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := dc.PullRequest(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\r\n\r\nfixes bug #2", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "#12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view pull request", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\r\n\r\nfixes bug #2", pl.ActionCard.Text)
+		assert.Equal(t, "#12 Fix bug", pl.ActionCard.Title)
+		assert.Equal(t, "view pull request", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := dc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\r\n\r\nchanges requested", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "#12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view issue comment", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\r\n\r\nchanges requested", pl.ActionCard.Text)
+		assert.Equal(t, "#12 Fix bug", pl.ActionCard.Title)
+		assert.Equal(t, "view issue comment", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(DingtalkPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := dc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view pull request", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug", pl.ActionCard.Title)
+		assert.Equal(t, "view pull request", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Repository(p)
+		pl, err := dc.Repository(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Repository created", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] Repository created", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view repository", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Repository created", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] Repository created", pl.ActionCard.Title)
+		assert.Equal(t, "view repository", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Package(p)
+		pl, err := dc.Package(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view package", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.ActionCard.Text)
+		assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.ActionCard.Title)
+		assert.Equal(t, "view package", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(DingtalkPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := dc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.ActionCard.Title)
+		assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL))
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = dc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.ActionCard.Title)
+		assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL))
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = dc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.ActionCard.Title)
+		assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(DingtalkPayload)
-		pl, err := d.Release(p)
+		pl, err := dc.Release(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DingtalkPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Text)
-		assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Title)
-		assert.Equal(t, "view release", pl.(*DingtalkPayload).ActionCard.SingleTitle)
-		assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+		assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.ActionCard.Text)
+		assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.ActionCard.Title)
+		assert.Equal(t, "view release", pl.ActionCard.SingleTitle)
+		assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.ActionCard.SingleURL))
 	})
 }
 
 func TestDingTalkJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(DingtalkPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &DingtalkPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.DINGTALK,
+		URL:        "https://dingtalk.example.com/",
+		Meta:       ``,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newDingtalkRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://dingtalk.example.com/", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body DingtalkPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.ActionCard.Text)
 }
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index e2ac1410b8..659754d5e0 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -4,8 +4,10 @@
 package webhook
 
 import (
+	"context"
 	"errors"
 	"fmt"
+	"net/http"
 	"net/url"
 	"strconv"
 	"strings"
@@ -98,19 +100,8 @@ var (
 	redColor         = color("ff3232")
 )
 
-// JSONPayload Marshals the DiscordPayload to json
-func (d *DiscordPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(d, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
-var _ PayloadConvertor = &DiscordPayload{}
-
 // Create implements PayloadConvertor Create method
-func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (d discordConvertor) Create(p *api.CreatePayload) (DiscordPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -119,7 +110,7 @@ func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
 }
 
 // Delete implements PayloadConvertor Delete method
-func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (d discordConvertor) Delete(p *api.DeletePayload) (DiscordPayload, error) {
 	// deleted tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -128,14 +119,14 @@ func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
 }
 
 // Fork implements PayloadConvertor Fork method
-func (d *DiscordPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (d discordConvertor) Fork(p *api.ForkPayload) (DiscordPayload, error) {
 	title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
 
 	return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL, greenColor), nil
 }
 
 // Push implements PayloadConvertor Push method
-func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
 	var (
 		branchName = git.RefName(p.Ref).ShortName()
 		commitDesc string
@@ -170,35 +161,35 @@ func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 }
 
 // Issue implements PayloadConvertor Issue method
-func (d *DiscordPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (d discordConvertor) Issue(p *api.IssuePayload) (DiscordPayload, error) {
 	title, _, text, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
 
 	return d.createPayload(p.Sender, title, text, p.Issue.HTMLURL, color), nil
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (d *DiscordPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (d discordConvertor) IssueComment(p *api.IssueCommentPayload) (DiscordPayload, error) {
 	title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)
 
 	return d.createPayload(p.Sender, title, p.Comment.Body, p.Comment.HTMLURL, color), nil
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (d *DiscordPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (d discordConvertor) PullRequest(p *api.PullRequestPayload) (DiscordPayload, error) {
 	title, _, text, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
 
 	return d.createPayload(p.Sender, title, text, p.PullRequest.HTMLURL, color), nil
 }
 
 // Review implements PayloadConvertor Review method
-func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (d discordConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DiscordPayload, error) {
 	var text, title string
 	var color int
 	switch p.Action {
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return DiscordPayload{}, err
 		}
 
 		title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -220,7 +211,7 @@ func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module.
 }
 
 // Repository implements PayloadConvertor Repository method
-func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (d discordConvertor) Repository(p *api.RepositoryPayload) (DiscordPayload, error) {
 	var title, url string
 	var color int
 	switch p.Action {
@@ -237,7 +228,7 @@ func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (d discordConvertor) Wiki(p *api.WikiPayload) (DiscordPayload, error) {
 	text, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
 	htmlLink := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)
 
@@ -250,30 +241,35 @@ func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
 }
 
 // Release implements PayloadConvertor Release method
-func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (d discordConvertor) Release(p *api.ReleasePayload) (DiscordPayload, error) {
 	text, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
 
 	return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil
 }
 
-func (d *DiscordPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error) {
 	text, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
 
 	return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
 }
 
-// GetDiscordPayload converts a discord webhook into a DiscordPayload
-func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
-	s := new(DiscordPayload)
+type discordConvertor struct {
+	Username  string
+	AvatarURL string
+}
 
-	discord := &DiscordMeta{}
-	if err := json.Unmarshal([]byte(meta), &discord); err != nil {
-		return s, errors.New("GetDiscordPayload meta json:" + err.Error())
+var _ payloadConvertor[DiscordPayload] = discordConvertor{}
+
+func newDiscordRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	meta := &DiscordMeta{}
+	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
+		return nil, nil, fmt.Errorf("newDiscordRequest meta json: %w", err)
 	}
-	s.Username = discord.Username
-	s.AvatarURL = discord.IconURL
-
-	return convertPayloader(s, p, event)
+	sc := discordConvertor{
+		Username:  meta.Username,
+		AvatarURL: meta.IconURL,
+	}
+	return newJSONRequest(sc, w, t, true)
 }
 
 func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
@@ -291,8 +287,8 @@ func parseHookPullRequestEventType(event webhook_module.HookEventType) (string,
 	}
 }
 
-func (d *DiscordPayload) createPayload(s *api.User, title, text, url string, color int) *DiscordPayload {
-	return &DiscordPayload{
+func (d discordConvertor) createPayload(s *api.User, title, text, url string, color int) DiscordPayload {
+	return DiscordPayload{
 		Username:  d.Username,
 		AvatarURL: d.AvatarURL,
 		Embeds: []DiscordEmbed{
diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go
index b567cbc395..c04b95383b 100644
--- a/services/webhook/discord_test.go
+++ b/services/webhook/discord_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -15,295 +18,274 @@ import (
 )
 
 func TestDiscordPayload(t *testing.T) {
+	dc := discordConvertor{}
+
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Create(p)
+		pl, err := dc.Create(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] branch test created", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] branch test created", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Delete(p)
+		pl, err := dc.Delete(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] branch test deleted", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] branch test deleted", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Fork(p)
+		pl, err := dc.Fork(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "test/repo2 is forked to test/repo", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Push(p)
+		pl, err := dc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
+		assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(DiscordPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := dc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "issue body", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Embeds[0].Title)
+		assert.Equal(t, "issue body", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = dc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := dc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "more info needed", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Embeds[0].Title)
+		assert.Equal(t, "more info needed", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := dc.PullRequest(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "fixes bug #2", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Embeds[0].Title)
+		assert.Equal(t, "fixes bug #2", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := dc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "changes requested", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Embeds[0].Title)
+		assert.Equal(t, "changes requested", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(DiscordPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := dc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "good job", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Embeds[0].Title)
+		assert.Equal(t, "good job", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Repository(p)
+		pl, err := dc.Repository(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Repository created", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Repository created", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Package(p)
+		pl, err := dc.Package(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "Package created: GiteaContainer:latest", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(DiscordPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := dc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "Wiki change comment", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Embeds[0].Title)
+		assert.Equal(t, "Wiki change comment", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = dc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "Wiki change comment", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Embeds[0].Title)
+		assert.Equal(t, "Wiki change comment", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = dc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Embeds[0].Title)
+		assert.Empty(t, pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(DiscordPayload)
-		pl, err := d.Release(p)
+		pl, err := dc.Release(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &DiscordPayload{}, pl)
 
-		assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
-		assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*DiscordPayload).Embeds[0].Title)
-		assert.Equal(t, "Note of first stable release", pl.(*DiscordPayload).Embeds[0].Description)
-		assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*DiscordPayload).Embeds[0].URL)
-		assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
-		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
-		assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+		assert.Len(t, pl.Embeds, 1)
+		assert.Equal(t, "[test/repo] Release created: v1.0", pl.Embeds[0].Title)
+		assert.Equal(t, "Note of first stable release", pl.Embeds[0].Description)
+		assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.Embeds[0].URL)
+		assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+		assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+		assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
 	})
 }
 
 func TestDiscordJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(DiscordPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &DiscordPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.DISCORD,
+		URL:        "https://discord.example.com/",
+		Meta:       `{}`,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newDiscordRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://discord.example.com/", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body DiscordPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Embeds[0].Description)
 }
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 556443e70b..1ec436894b 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -4,11 +4,13 @@
 package webhook
 
 import (
+	"context"
 	"fmt"
+	"net/http"
 	"strings"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 )
@@ -23,8 +25,8 @@ type (
 	}
 )
 
-func newFeishuTextPayload(text string) *FeishuPayload {
-	return &FeishuPayload{
+func newFeishuTextPayload(text string) FeishuPayload {
+	return FeishuPayload{
 		MsgType: "text",
 		Content: struct {
 			Text string `json:"text"`
@@ -34,19 +36,8 @@ func newFeishuTextPayload(text string) *FeishuPayload {
 	}
 }
 
-// JSONPayload Marshals the FeishuPayload to json
-func (f *FeishuPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(f, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
-var _ PayloadConvertor = &FeishuPayload{}
-
 // Create implements PayloadConvertor Create method
-func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Create(p *api.CreatePayload) (FeishuPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	text := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -55,7 +46,7 @@ func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
 }
 
 // Delete implements PayloadConvertor Delete method
-func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Delete(p *api.DeletePayload) (FeishuPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	text := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -64,14 +55,14 @@ func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
 }
 
 // Fork implements PayloadConvertor Fork method
-func (f *FeishuPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (fc feishuConvertor) Fork(p *api.ForkPayload) (FeishuPayload, error) {
 	text := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
 
 	return newFeishuTextPayload(text), nil
 }
 
 // Push implements PayloadConvertor Push method
-func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) {
 	var (
 		branchName = git.RefName(p.Ref).ShortName()
 		commitDesc string
@@ -96,48 +87,40 @@ func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 }
 
 // Issue implements PayloadConvertor Issue method
-func (f *FeishuPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Issue(p *api.IssuePayload) (FeishuPayload, error) {
 	title, link, by, operator, result, assignees := getIssuesInfo(p)
-	var res api.Payloader
 	if assignees != "" {
 		if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
-			res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body))
-		} else {
-			res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body))
+			return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body)), nil
 		}
-	} else {
-		res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body))
+		return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body)), nil
 	}
-	return res, nil
+	return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body)), nil
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (f *FeishuPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (fc feishuConvertor) IssueComment(p *api.IssueCommentPayload) (FeishuPayload, error) {
 	title, link, by, operator := getIssuesCommentInfo(p)
 	return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Comment.Body)), nil
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (f *FeishuPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (fc feishuConvertor) PullRequest(p *api.PullRequestPayload) (FeishuPayload, error) {
 	title, link, by, operator, result, assignees := getPullRequestInfo(p)
-	var res api.Payloader
 	if assignees != "" {
 		if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
-			res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body))
-		} else {
-			res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body))
+			return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body)), nil
 		}
-	} else {
-		res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body))
+		return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body)), nil
 	}
-	return res, nil
+	return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body)), nil
 }
 
 // Review implements PayloadConvertor Review method
-func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (fc feishuConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (FeishuPayload, error) {
 	action, err := parseHookPullRequestEventType(event)
 	if err != nil {
-		return nil, err
+		return FeishuPayload{}, err
 	}
 
 	title := fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -147,7 +130,7 @@ func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.H
 }
 
 // Repository implements PayloadConvertor Repository method
-func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (fc feishuConvertor) Repository(p *api.RepositoryPayload) (FeishuPayload, error) {
 	var text string
 	switch p.Action {
 	case api.HookRepoCreated:
@@ -158,30 +141,33 @@ func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
 		return newFeishuTextPayload(text), nil
 	}
 
-	return nil, nil
+	return FeishuPayload{}, nil
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (f *FeishuPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (fc feishuConvertor) Wiki(p *api.WikiPayload) (FeishuPayload, error) {
 	text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
 
 	return newFeishuTextPayload(text), nil
 }
 
 // Release implements PayloadConvertor Release method
-func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Release(p *api.ReleasePayload) (FeishuPayload, error) {
 	text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
 
 	return newFeishuTextPayload(text), nil
 }
 
-func (f *FeishuPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error) {
 	text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
 
 	return newFeishuTextPayload(text), nil
 }
 
-// GetFeishuPayload converts a ding talk webhook into a FeishuPayload
-func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
-	return convertPayloader(new(FeishuPayload), p, event)
+type feishuConvertor struct{}
+
+var _ payloadConvertor[FeishuPayload] = feishuConvertor{}
+
+func newFeishuRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	return newJSONRequest(feishuConvertor{}, w, t, true)
 }
diff --git a/services/webhook/feishu_test.go b/services/webhook/feishu_test.go
index 98bc50dede..ef18333fd4 100644
--- a/services/webhook/feishu_test.go
+++ b/services/webhook/feishu_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -14,199 +17,177 @@ import (
 )
 
 func TestFeishuPayload(t *testing.T) {
+	fc := feishuConvertor{}
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Create(p)
+		pl, err := fc.Create(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, `[test/repo] branch test created`, pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, `[test/repo] branch test created`, pl.Content.Text)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Delete(p)
+		pl, err := fc.Delete(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, `[test/repo] branch test deleted`, pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, `[test/repo] branch test deleted`, pl.Content.Text)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Fork(p)
+		pl, err := fc.Fork(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, `test/repo2 is forked to test/repo`, pl.Content.Text)
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Push(p)
+		pl, err := fc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Content.Text)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(FeishuPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := fc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.Content.Text)
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = fc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.Content.Text)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := fc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.Content.Text)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := fc.PullRequest(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.Content.Text)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := fc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.Content.Text)
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(FeishuPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := fc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.Content.Text)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Repository(p)
+		pl, err := fc.Repository(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Repository created", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo] Repository created", pl.Content.Text)
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Package(p)
+		pl, err := fc.Package(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.Content.Text)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(FeishuPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := fc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.Content.Text)
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = fc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.Content.Text)
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = fc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.Content.Text)
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(FeishuPayload)
-		pl, err := d.Release(p)
+		pl, err := fc.Release(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &FeishuPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*FeishuPayload).Content.Text)
+		assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.Content.Text)
 	})
 }
 
 func TestFeishuJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(FeishuPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &FeishuPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.FEISHU,
+		URL:        "https://feishu.example.com/",
+		Meta:       `{}`,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newFeishuRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://feishu.example.com/", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body FeishuPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Content.Text)
 }
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 602d16ef39..0329804a8b 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -4,11 +4,12 @@
 package webhook
 
 import (
+	"bytes"
+	"context"
 	"crypto/sha1"
 	"encoding/hex"
-	"errors"
 	"fmt"
-	"html"
+	"net/http"
 	"net/url"
 	"regexp"
 	"strings"
@@ -23,6 +24,37 @@ import (
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 )
 
+func newMatrixRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	meta := &MatrixMeta{}
+	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
+		return nil, nil, fmt.Errorf("GetMatrixPayload meta json: %w", err)
+	}
+	mc := matrixConvertor{
+		MsgType: messageTypeText[meta.MessageType],
+	}
+	payload, err := newPayload(mc, []byte(t.PayloadContent), t.EventType)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	body, err := json.MarshalIndent(payload, "", "  ")
+	if err != nil {
+		return nil, nil, err
+	}
+
+	txnID, err := getMatrixTxnID(body)
+	if err != nil {
+		return nil, nil, err
+	}
+	req, err := http.NewRequest(http.MethodPut, w.URL+"/"+txnID, bytes.NewReader(body))
+	if err != nil {
+		return nil, nil, err
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially
+}
+
 const matrixPayloadSizeLimit = 1024 * 64
 
 // MatrixMeta contains the Matrix metadata
@@ -46,8 +78,6 @@ func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta {
 	return s
 }
 
-var _ PayloadConvertor = &MatrixPayload{}
-
 // MatrixPayload contains payload for a Matrix room
 type MatrixPayload struct {
 	Body          string               `json:"body"`
@@ -57,90 +87,79 @@ type MatrixPayload struct {
 	Commits       []*api.PayloadCommit `json:"io.gitea.commits,omitempty"`
 }
 
-// JSONPayload Marshals the MatrixPayload to json
-func (m *MatrixPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(m, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
+var _ payloadConvertor[MatrixPayload] = matrixConvertor{}
+
+type matrixConvertor struct {
+	MsgType string
 }
 
-// MatrixLinkFormatter creates a link compatible with Matrix
-func MatrixLinkFormatter(url, text string) string {
-	return fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(url), html.EscapeString(text))
+func (m matrixConvertor) newPayload(text string, commits ...*api.PayloadCommit) (MatrixPayload, error) {
+	return MatrixPayload{
+		Body:          getMessageBody(text),
+		MsgType:       m.MsgType,
+		Format:        "org.matrix.custom.html",
+		FormattedBody: text,
+		Commits:       commits,
+	}, nil
 }
 
-// MatrixLinkToRef Matrix-formatter link to a repo ref
-func MatrixLinkToRef(repoURL, ref string) string {
-	refName := git.RefName(ref).ShortName()
-	switch {
-	case strings.HasPrefix(ref, git.BranchPrefix):
-		return MatrixLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName)
-	case strings.HasPrefix(ref, git.TagPrefix):
-		return MatrixLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName)
-	default:
-		return MatrixLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName)
-	}
-}
-
-// Create implements PayloadConvertor Create method
-func (m *MatrixPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
-	repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
+// Create implements payloadConvertor Create method
+func (m matrixConvertor) Create(p *api.CreatePayload) (MatrixPayload, error) {
+	repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	refLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
 	text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
 // Delete composes Matrix payload for delete a branch or tag.
-func (m *MatrixPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (m matrixConvertor) Delete(p *api.DeletePayload) (MatrixPayload, error) {
 	refName := git.RefName(p.Ref).ShortName()
-	repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
+	repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
 // Fork composes Matrix payload for forked by a repository.
-func (m *MatrixPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
-	baseLink := MatrixLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
-	forkLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
+func (m matrixConvertor) Fork(p *api.ForkPayload) (MatrixPayload, error) {
+	baseLink := htmlLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
+	forkLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// Issue implements PayloadConvertor Issue method
-func (m *MatrixPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
-	text, _, _, _ := getIssuesPayloadInfo(p, MatrixLinkFormatter, true)
+// Issue implements payloadConvertor Issue method
+func (m matrixConvertor) Issue(p *api.IssuePayload) (MatrixPayload, error) {
+	text, _, _, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// IssueComment implements PayloadConvertor IssueComment method
-func (m *MatrixPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
-	text, _, _ := getIssueCommentPayloadInfo(p, MatrixLinkFormatter, true)
+// IssueComment implements payloadConvertor IssueComment method
+func (m matrixConvertor) IssueComment(p *api.IssueCommentPayload) (MatrixPayload, error) {
+	text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// Wiki implements PayloadConvertor Wiki method
-func (m *MatrixPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
-	text, _, _ := getWikiPayloadInfo(p, MatrixLinkFormatter, true)
+// Wiki implements payloadConvertor Wiki method
+func (m matrixConvertor) Wiki(p *api.WikiPayload) (MatrixPayload, error) {
+	text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// Release implements PayloadConvertor Release method
-func (m *MatrixPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
-	text, _ := getReleasePayloadInfo(p, MatrixLinkFormatter, true)
+// Release implements payloadConvertor Release method
+func (m matrixConvertor) Release(p *api.ReleasePayload) (MatrixPayload, error) {
+	text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// Push implements PayloadConvertor Push method
-func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+// Push implements payloadConvertor Push method
+func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) {
 	var commitDesc string
 
 	if p.TotalCommits == 1 {
@@ -149,13 +168,13 @@ func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 		commitDesc = fmt.Sprintf("%d commits", p.TotalCommits)
 	}
 
-	repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
+	repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
 	text := fmt.Sprintf("[%s] %s pushed %s to %s:<br>", repoLink, p.Pusher.UserName, commitDesc, branchLink)
 
 	// for each commit, generate a new line text
 	for i, commit := range p.Commits {
-		text += fmt.Sprintf("%s: %s - %s", MatrixLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)
+		text += fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)
 		// add linebreak to each commit but the last
 		if i < len(p.Commits)-1 {
 			text += "<br>"
@@ -163,41 +182,41 @@ func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 
 	}
 
-	return getMatrixPayload(text, p.Commits, m.MsgType), nil
+	return m.newPayload(text, p.Commits...)
 }
 
-// PullRequest implements PayloadConvertor PullRequest method
-func (m *MatrixPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
-	text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true)
+// PullRequest implements payloadConvertor PullRequest method
+func (m matrixConvertor) PullRequest(p *api.PullRequestPayload) (MatrixPayload, error) {
+	text, _, _, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true)
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// Review implements PayloadConvertor Review method
-func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
-	senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
+// Review implements payloadConvertor Review method
+func (m matrixConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MatrixPayload, error) {
+	senderLink := htmlLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
 	title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
-	titleLink := MatrixLinkFormatter(p.PullRequest.HTMLURL, title)
-	repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
+	titleLink := htmlLinkFormatter(p.PullRequest.HTMLURL, title)
+	repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
 	var text string
 
 	switch p.Action {
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return MatrixPayload{}, err
 		}
 
 		text = fmt.Sprintf("[%s] Pull request review %s: %s by %s", repoLink, action, titleLink, senderLink)
 	}
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-// Repository implements PayloadConvertor Repository method
-func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
-	senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
-	repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
+// Repository implements payloadConvertor Repository method
+func (m matrixConvertor) Repository(p *api.RepositoryPayload) (MatrixPayload, error) {
+	senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
+	repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
 	var text string
 
 	switch p.Action {
@@ -206,13 +225,12 @@ func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
 	case api.HookRepoDeleted:
 		text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
 	}
-
-	return getMatrixPayload(text, nil, m.MsgType), nil
+	return m.newPayload(text)
 }
 
-func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
-	senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
-	packageLink := MatrixLinkFormatter(p.Package.HTMLURL, p.Package.Name)
+func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) {
+	senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
+	packageLink := htmlLinkFormatter(p.Package.HTMLURL, p.Package.Name)
 	var text string
 
 	switch p.Action {
@@ -222,31 +240,7 @@ func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
 		text = fmt.Sprintf("[%s] Package deleted by %s", packageLink, senderLink)
 	}
 
-	return getMatrixPayload(text, nil, m.MsgType), nil
-}
-
-// GetMatrixPayload converts a Matrix webhook into a MatrixPayload
-func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
-	s := new(MatrixPayload)
-
-	matrix := &MatrixMeta{}
-	if err := json.Unmarshal([]byte(meta), &matrix); err != nil {
-		return s, errors.New("GetMatrixPayload meta json:" + err.Error())
-	}
-
-	s.MsgType = messageTypeText[matrix.MessageType]
-
-	return convertPayloader(s, p, event)
-}
-
-func getMatrixPayload(text string, commits []*api.PayloadCommit, msgType string) *MatrixPayload {
-	p := MatrixPayload{}
-	p.FormattedBody = text
-	p.Body = getMessageBody(text)
-	p.Format = "org.matrix.custom.html"
-	p.MsgType = msgType
-	p.Commits = commits
-	return &p
+	return m.newPayload(text)
 }
 
 var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
@@ -271,3 +265,16 @@ func getMatrixTxnID(payload []byte) (string, error) {
 
 	return hex.EncodeToString(h.Sum(nil)), nil
 }
+
+// MatrixLinkToRef Matrix-formatter link to a repo ref
+func MatrixLinkToRef(repoURL, ref string) string {
+	refName := git.RefName(ref).ShortName()
+	switch {
+	case strings.HasPrefix(ref, git.BranchPrefix):
+		return htmlLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName)
+	case strings.HasPrefix(ref, git.TagPrefix):
+		return htmlLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName)
+	default:
+		return htmlLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName)
+	}
+}
diff --git a/services/webhook/matrix_test.go b/services/webhook/matrix_test.go
index 99a22fbd7e..058f8e3c5f 100644
--- a/services/webhook/matrix_test.go
+++ b/services/webhook/matrix_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -14,217 +17,213 @@ import (
 )
 
 func TestMatrixPayload(t *testing.T) {
+	mc := matrixConvertor{
+		MsgType: "m.text",
+	}
+
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Create(p)
+		pl, err := mc.Create(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:<a href="http://localhost:3000/test/repo/src/branch/test">test</a>] branch created by user1`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:<a href="http://localhost:3000/test/repo/src/branch/test">test</a>] branch created by user1`, pl.FormattedBody)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Delete(p)
+		pl, err := mc.Delete(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:test] branch deleted by user1`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:test] branch deleted by user1`, pl.FormattedBody)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Fork(p)
+		pl, err := mc.Fork(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `<a href="http://localhost:3000/test/repo2">test/repo2</a> is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.Body)
+		assert.Equal(t, `<a href="http://localhost:3000/test/repo2">test/repo2</a> is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, pl.FormattedBody)
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Push(p)
+		pl, err := mc.Push(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] user1 pushed 2 commits to <a href="http://localhost:3000/test/repo/src/branch/test">test</a>:<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] user1 pushed 2 commits to <a href="http://localhost:3000/test/repo/src/branch/test">test</a>:<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1`, pl.FormattedBody)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(MatrixPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := mc.Issue(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue opened: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue opened: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = mc.Issue(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := mc.IssueComment(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on issue <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on issue <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := mc.PullRequest(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request opened: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request opened: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := mc.IssueComment(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on pull request <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on pull request <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(MatrixPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request review approved: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request review approved: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Repository(p)
+		pl, err := mc.Repository(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Package(p)
+		pl, err := mc.Package(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, `[[GiteaContainer](http://localhost:3000/user1/-/packages/container/GiteaContainer/latest)] Package published by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer</a>] Package published by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, `[[GiteaContainer](http://localhost:3000/user1/-/packages/container/GiteaContainer/latest)] Package published by [user1](https://try.gitea.io/user1)`, pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer</a>] Package published by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(MatrixPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := mc.Wiki(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = mc.Wiki(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = mc.Wiki(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(MatrixPayload)
-		pl, err := d.Release(p)
+		pl, err := mc.Release(p)
 		require.NoError(t, err)
 		require.NotNil(t, pl)
-		require.IsType(t, &MatrixPayload{}, pl)
 
-		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
+		assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.Body)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
 	})
 }
 
 func TestMatrixJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(MatrixPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &MatrixPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:   3,
+		IsActive: true,
+		Type:     webhook_module.MATRIX,
+		URL:      "https://matrix.example.com/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message",
+		Meta:     `{"message_type":0}`, // text
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newMatrixRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "PUT", req.Method)
+	assert.Equal(t, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/6db5dc1e282529a8c162c7fe93dd2667494eeb51", req.URL.Path)
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body MatrixPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", body.Body)
 }
 
 func Test_getTxnID(t *testing.T) {
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 37810b4cd3..99d0106184 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -4,12 +4,14 @@
 package webhook
 
 import (
+	"context"
 	"fmt"
+	"net/http"
 	"net/url"
 	"strings"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -56,19 +58,8 @@ type (
 	}
 )
 
-// JSONPayload Marshals the MSTeamsPayload to json
-func (m *MSTeamsPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(m, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
-var _ PayloadConvertor = &MSTeamsPayload{}
-
 // Create implements PayloadConvertor Create method
-func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Create(p *api.CreatePayload) (MSTeamsPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -85,7 +76,7 @@ func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
 }
 
 // Delete implements PayloadConvertor Delete method
-func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Delete(p *api.DeletePayload) (MSTeamsPayload, error) {
 	// deleted tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -102,7 +93,7 @@ func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
 }
 
 // Fork implements PayloadConvertor Fork method
-func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (m msteamsConvertor) Fork(p *api.ForkPayload) (MSTeamsPayload, error) {
 	title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
 
 	return createMSTeamsPayload(
@@ -117,7 +108,7 @@ func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
 }
 
 // Push implements PayloadConvertor Push method
-func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) {
 	var (
 		branchName = git.RefName(p.Ref).ShortName()
 		commitDesc string
@@ -160,7 +151,7 @@ func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 }
 
 // Issue implements PayloadConvertor Issue method
-func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Issue(p *api.IssuePayload) (MSTeamsPayload, error) {
 	title, _, attachmentText, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
 
 	return createMSTeamsPayload(
@@ -175,7 +166,7 @@ func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (m msteamsConvertor) IssueComment(p *api.IssueCommentPayload) (MSTeamsPayload, error) {
 	title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)
 
 	return createMSTeamsPayload(
@@ -190,7 +181,7 @@ func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload, error) {
 	title, _, attachmentText, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
 
 	return createMSTeamsPayload(
@@ -205,14 +196,14 @@ func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader,
 }
 
 // Review implements PayloadConvertor Review method
-func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (m msteamsConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MSTeamsPayload, error) {
 	var text, title string
 	var color int
 	switch p.Action {
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return MSTeamsPayload{}, err
 		}
 
 		title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -242,7 +233,7 @@ func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module.
 }
 
 // Repository implements PayloadConvertor Repository method
-func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (m msteamsConvertor) Repository(p *api.RepositoryPayload) (MSTeamsPayload, error) {
 	var title, url string
 	var color int
 	switch p.Action {
@@ -267,7 +258,7 @@ func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (m msteamsConvertor) Wiki(p *api.WikiPayload) (MSTeamsPayload, error) {
 	title, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
 
 	return createMSTeamsPayload(
@@ -282,7 +273,7 @@ func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
 }
 
 // Release implements PayloadConvertor Release method
-func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Release(p *api.ReleasePayload) (MSTeamsPayload, error) {
 	title, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
 
 	return createMSTeamsPayload(
@@ -296,7 +287,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
 	), nil
 }
 
-func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error) {
 	title, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
 
 	return createMSTeamsPayload(
@@ -310,12 +301,7 @@ func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
 	), nil
 }
 
-// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload
-func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
-	return convertPayloader(new(MSTeamsPayload), p, event)
-}
-
-func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) *MSTeamsPayload {
+func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
 	facts := make([]MSTeamsFact, 0, 2)
 	if r != nil {
 		facts = append(facts, MSTeamsFact{
@@ -327,7 +313,7 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar
 		facts = append(facts, *fact)
 	}
 
-	return &MSTeamsPayload{
+	return MSTeamsPayload{
 		Type:       "MessageCard",
 		Context:    "https://schema.org/extensions",
 		ThemeColor: fmt.Sprintf("%x", color),
@@ -356,3 +342,11 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar
 		},
 	}
 }
+
+type msteamsConvertor struct{}
+
+var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{}
+
+func newMSTeamsRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	return newJSONRequest(msteamsConvertor{}, w, t, true)
+}
diff --git a/services/webhook/msteams_test.go b/services/webhook/msteams_test.go
index 8d1aed6040..01e08b918e 100644
--- a/services/webhook/msteams_test.go
+++ b/services/webhook/msteams_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -14,22 +17,20 @@ import (
 )
 
 func TestMSTeamsPayload(t *testing.T) {
+	mc := msteamsConvertor{}
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Create(p)
+		pl, err := mc.Create(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] branch test created", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] branch test created", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] branch test created", pl.Title)
+		assert.Equal(t, "[test/repo] branch test created", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repo.FullName, fact.Value)
 			} else if fact.Name == "branch:" {
@@ -38,27 +39,24 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Delete(p)
+		pl, err := mc.Delete(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] branch test deleted", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] branch test deleted", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] branch test deleted", pl.Title)
+		assert.Equal(t, "[test/repo] branch test deleted", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repo.FullName, fact.Value)
 			} else if fact.Name == "branch:" {
@@ -67,27 +65,24 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Fork(p)
+		pl, err := mc.Fork(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "test/repo2 is forked to test/repo", pl.Title)
+		assert.Equal(t, "test/repo2 is forked to test/repo", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repo.FullName, fact.Value)
 			} else if fact.Name == "Forkee:" {
@@ -96,27 +91,24 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Push(p)
+		pl, err := mc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo:test] 2 new commits", pl.Title)
+		assert.Equal(t, "[test/repo:test] 2 new commits", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repo.FullName, fact.Value)
 			} else if fact.Name == "Commit count:" {
@@ -125,28 +117,25 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(MSTeamsPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := mc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "issue body", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Title)
+		assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "issue body", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Issue #:" {
@@ -155,23 +144,21 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI)
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = mc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Title)
+		assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Issue #:" {
@@ -180,27 +167,24 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := mc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "more info needed", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Title)
+		assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "more info needed", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Issue #:" {
@@ -209,27 +193,24 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := mc.PullRequest(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "fixes bug #2", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Title)
+		assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "fixes bug #2", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Pull request #:" {
@@ -238,27 +219,24 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := mc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "changes requested", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Title)
+		assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "changes requested", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Issue #:" {
@@ -267,28 +245,25 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "good job", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Title)
+		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "good job", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Pull request #:" {
@@ -297,155 +272,139 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Repository(p)
+		pl, err := mc.Repository(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Repository created", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Repository created", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 1)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Repository created", pl.Title)
+		assert.Equal(t, "[test/repo] Repository created", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 1)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Package(p)
+		pl, err := mc.Package(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 1)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "Package created: GiteaContainer:latest", pl.Title)
+		assert.Equal(t, "Package created: GiteaContainer:latest", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 1)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Package:" {
 				assert.Equal(t, p.Package.Name, fact.Value)
 			} else {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(MSTeamsPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := mc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Title)
+		assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = mc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Title)
+		assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Equal(t, "", pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = mc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Title)
+		assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(MSTeamsPayload)
-		pl, err := d.Release(p)
+		pl, err := mc.Release(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &MSTeamsPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*MSTeamsPayload).Title)
-		assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*MSTeamsPayload).Summary)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
-		assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
-		assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
-		assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
-		for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+		assert.Equal(t, "[test/repo] Release created: v1.0", pl.Title)
+		assert.Equal(t, "[test/repo] Release created: v1.0", pl.Summary)
+		assert.Len(t, pl.Sections, 1)
+		assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+		assert.Empty(t, pl.Sections[0].Text)
+		assert.Len(t, pl.Sections[0].Facts, 2)
+		for _, fact := range pl.Sections[0].Facts {
 			if fact.Name == "Repository:" {
 				assert.Equal(t, p.Repository.FullName, fact.Value)
 			} else if fact.Name == "Tag:" {
@@ -454,21 +413,43 @@ func TestMSTeamsPayload(t *testing.T) {
 				t.Fail()
 			}
 		}
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
-		assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
-		assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+		assert.Len(t, pl.PotentialAction, 1)
+		assert.Len(t, pl.PotentialAction[0].Targets, 1)
+		assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.PotentialAction[0].Targets[0].URI)
 	})
 }
 
 func TestMSTeamsJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(MSTeamsPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &MSTeamsPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.MSTEAMS,
+		URL:        "https://msteams.example.com/",
+		Meta:       ``,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newMSTeamsRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://msteams.example.com/", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body MSTeamsPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[test/repo:test] 2 new commits", body.Summary)
 }
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index d9d3bc1dd5..2a8e663637 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -67,7 +67,7 @@ func (m *webhookNotifier) IssueClearLabels(ctx context.Context, doer *user_model
 		err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueLabel, &api.IssuePayload{
 			Action:     api.HookIssueLabelCleared,
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		})
@@ -168,7 +168,7 @@ func (m *webhookNotifier) IssueChangeAssignee(ctx context.Context, doer *user_mo
 		permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
 		apiIssue := &api.IssuePayload{
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		}
@@ -214,7 +214,7 @@ func (m *webhookNotifier) IssueChangeTitle(ctx context.Context, doer *user_model
 					From: oldTitle,
 				},
 			},
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		})
@@ -250,7 +250,7 @@ func (m *webhookNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode
 	} else {
 		apiIssue := &api.IssuePayload{
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 			CommitID:   commitID,
@@ -281,7 +281,7 @@ func (m *webhookNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu
 	if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssues, &api.IssuePayload{
 		Action:     api.HookIssueOpened,
 		Index:      issue.Index,
-		Issue:      convert.ToAPIIssue(ctx, issue),
+		Issue:      convert.ToAPIIssue(ctx, issue.Poster, issue),
 		Repository: convert.ToRepo(ctx, issue.Repo, permission),
 		Sender:     convert.ToUser(ctx, issue.Poster, nil),
 	}); err != nil {
@@ -349,7 +349,7 @@ func (m *webhookNotifier) IssueChangeContent(ctx context.Context, doer *user_mod
 					From: oldContent,
 				},
 			},
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		})
@@ -384,7 +384,7 @@ func (m *webhookNotifier) UpdateComment(ctx context.Context, doer *user_model.Us
 	permission, _ := access_model.GetUserRepoPermission(ctx, c.Issue.Repo, doer)
 	if err := PrepareWebhooks(ctx, EventSource{Repository: c.Issue.Repo}, eventType, &api.IssueCommentPayload{
 		Action:  api.HookIssueCommentEdited,
-		Issue:   convert.ToAPIIssue(ctx, c.Issue),
+		Issue:   convert.ToAPIIssue(ctx, doer, c.Issue),
 		Comment: convert.ToAPIComment(ctx, c.Issue.Repo, c),
 		Changes: &api.ChangesPayload{
 			Body: &api.ChangesFromPayload{
@@ -412,7 +412,7 @@ func (m *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_mod
 	permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer)
 	if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, eventType, &api.IssueCommentPayload{
 		Action:     api.HookIssueCommentCreated,
-		Issue:      convert.ToAPIIssue(ctx, issue),
+		Issue:      convert.ToAPIIssue(ctx, doer, issue),
 		Comment:    convert.ToAPIComment(ctx, repo, comment),
 		Repository: convert.ToRepo(ctx, repo, permission),
 		Sender:     convert.ToUser(ctx, doer, nil),
@@ -449,7 +449,7 @@ func (m *webhookNotifier) DeleteComment(ctx context.Context, doer *user_model.Us
 	permission, _ := access_model.GetUserRepoPermission(ctx, comment.Issue.Repo, doer)
 	if err := PrepareWebhooks(ctx, EventSource{Repository: comment.Issue.Repo}, eventType, &api.IssueCommentPayload{
 		Action:     api.HookIssueCommentDeleted,
-		Issue:      convert.ToAPIIssue(ctx, comment.Issue),
+		Issue:      convert.ToAPIIssue(ctx, doer, comment.Issue),
 		Comment:    convert.ToAPIComment(ctx, comment.Issue.Repo, comment),
 		Repository: convert.ToRepo(ctx, comment.Issue.Repo, permission),
 		Sender:     convert.ToUser(ctx, doer, nil),
@@ -533,7 +533,7 @@ func (m *webhookNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
 		err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueLabel, &api.IssuePayload{
 			Action:     api.HookIssueLabelUpdated,
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		})
@@ -575,7 +575,7 @@ func (m *webhookNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m
 		err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueMilestone, &api.IssuePayload{
 			Action:     hookAction,
 			Index:      issue.Index,
-			Issue:      convert.ToAPIIssue(ctx, issue),
+			Issue:      convert.ToAPIIssue(ctx, doer, issue),
 			Repository: convert.ToRepo(ctx, issue.Repo, permission),
 			Sender:     convert.ToUser(ctx, doer, nil),
 		})
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index 714a4c076e..7880d8b606 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -4,7 +4,9 @@
 package webhook
 
 import (
-	"errors"
+	"context"
+	"fmt"
+	"net/http"
 
 	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/json"
@@ -38,84 +40,85 @@ func GetPackagistHook(w *webhook_model.Webhook) *PackagistMeta {
 	return s
 }
 
-// JSONPayload Marshals the PackagistPayload to json
-func (f *PackagistPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(f, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
-var _ PayloadConvertor = &PackagistPayload{}
-
 // Create implements PayloadConvertor Create method
-func (f *PackagistPayload) Create(_ *api.CreatePayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Create(_ *api.CreatePayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Delete implements PayloadConvertor Delete method
-func (f *PackagistPayload) Delete(_ *api.DeletePayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Delete(_ *api.DeletePayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Fork implements PayloadConvertor Fork method
-func (f *PackagistPayload) Fork(_ *api.ForkPayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Fork(_ *api.ForkPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Push implements PayloadConvertor Push method
-func (f *PackagistPayload) Push(_ *api.PushPayload) (api.Payloader, error) {
-	return f, nil
+// https://packagist.org/about
+func (pc packagistConvertor) Push(_ *api.PushPayload) (PackagistPayload, error) {
+	return PackagistPayload{
+		PackagistRepository: struct {
+			URL string `json:"url"`
+		}{
+			URL: pc.PackageURL,
+		},
+	}, nil
 }
 
 // Issue implements PayloadConvertor Issue method
-func (f *PackagistPayload) Issue(_ *api.IssuePayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Issue(_ *api.IssuePayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (f *PackagistPayload) IssueComment(_ *api.IssueCommentPayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) IssueComment(_ *api.IssueCommentPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (f *PackagistPayload) PullRequest(_ *api.PullRequestPayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) PullRequest(_ *api.PullRequestPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Review implements PayloadConvertor Review method
-func (f *PackagistPayload) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Repository implements PayloadConvertor Repository method
-func (f *PackagistPayload) Repository(_ *api.RepositoryPayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Repository(_ *api.RepositoryPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (f *PackagistPayload) Wiki(_ *api.WikiPayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Wiki(_ *api.WikiPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
 // Release implements PayloadConvertor Release method
-func (f *PackagistPayload) Release(_ *api.ReleasePayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Release(_ *api.ReleasePayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
-func (f *PackagistPayload) Package(_ *api.PackagePayload) (api.Payloader, error) {
-	return nil, nil
+func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
 }
 
-// GetPackagistPayload converts a packagist webhook into a PackagistPayload
-func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
-	s := new(PackagistPayload)
+type packagistConvertor struct {
+	PackageURL string
+}
 
-	packagist := &PackagistMeta{}
-	if err := json.Unmarshal([]byte(meta), &packagist); err != nil {
-		return s, errors.New("GetPackagistPayload meta json:" + err.Error())
+var _ payloadConvertor[PackagistPayload] = packagistConvertor{}
+
+func newPackagistRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	meta := &PackagistMeta{}
+	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
+		return nil, nil, fmt.Errorf("newpackagistRequest meta json: %w", err)
 	}
-	s.PackagistRepository.URL = packagist.PackageURL
-	return convertPayloader(s, p, event)
+	pc := packagistConvertor{
+		PackageURL: meta.PackageURL,
+	}
+	return newJSONRequest(pc, w, t, true)
 }
diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go
index 26d01b0555..e9b0695baa 100644
--- a/services/webhook/packagist_test.go
+++ b/services/webhook/packagist_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -14,155 +17,199 @@ import (
 )
 
 func TestPackagistPayload(t *testing.T) {
+	pc := packagistConvertor{
+		PackageURL: "https://packagist.org/packages/example",
+	}
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.Create(p)
+		pl, err := pc.Create(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.Delete(p)
+		pl, err := pc.Delete(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.Fork(p)
+		pl, err := pc.Fork(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(PackagistPayload)
-		d.PackagistRepository.URL = "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN"
-		pl, err := d.Push(p)
+		pl, err := pc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &PackagistPayload{}, pl)
 
-		assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", pl.(*PackagistPayload).PackagistRepository.URL)
+		assert.Equal(t, "https://packagist.org/packages/example", pl.PackagistRepository.URL)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(PackagistPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := pc.Issue(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = pc.Issue(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := pc.IssueComment(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := pc.PullRequest(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := pc.IssueComment(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(PackagistPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.Repository(p)
+		pl, err := pc.Repository(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.Package(p)
+		pl, err := pc.Package(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(PackagistPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := pc.Wiki(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = pc.Wiki(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = pc.Wiki(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(PackagistPayload)
-		pl, err := d.Release(p)
+		pl, err := pc.Release(p)
 		require.NoError(t, err)
-		require.Nil(t, pl)
+		require.Equal(t, pl, PackagistPayload{})
 	})
 }
 
 func TestPackagistJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(PackagistPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &PackagistPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.PACKAGIST,
+		URL:        "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN",
+		Meta:       `{"package_url":"https://packagist.org/packages/example"}`,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newPackagistRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body PackagistPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "https://packagist.org/packages/example", body.PackagistRepository.URL)
+}
+
+func TestPackagistEmptyPayload(t *testing.T) {
+	p := createTestPayload()
+	data, err := p.JSONPayload()
+	require.NoError(t, err)
+
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.PACKAGIST,
+		URL:        "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN",
+		Meta:       `{"package_url":"https://packagist.org/packages/example"}`,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventCreate,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newPackagistRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
+	require.NoError(t, err)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body PackagistPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "", body.PackagistRepository.URL)
 }
diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index bd482c04ea..54a11a5868 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/payloader.go
@@ -4,58 +4,109 @@
 package webhook
 
 import (
+	"bytes"
+	"fmt"
+	"net/http"
+
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 )
 
-// PayloadConvertor defines the interface to convert system webhook payload to external payload
-type PayloadConvertor interface {
-	api.Payloader
-	Create(*api.CreatePayload) (api.Payloader, error)
-	Delete(*api.DeletePayload) (api.Payloader, error)
-	Fork(*api.ForkPayload) (api.Payloader, error)
-	Issue(*api.IssuePayload) (api.Payloader, error)
-	IssueComment(*api.IssueCommentPayload) (api.Payloader, error)
-	Push(*api.PushPayload) (api.Payloader, error)
-	PullRequest(*api.PullRequestPayload) (api.Payloader, error)
-	Review(*api.PullRequestPayload, webhook_module.HookEventType) (api.Payloader, error)
-	Repository(*api.RepositoryPayload) (api.Payloader, error)
-	Release(*api.ReleasePayload) (api.Payloader, error)
-	Wiki(*api.WikiPayload) (api.Payloader, error)
-	Package(*api.PackagePayload) (api.Payloader, error)
+// payloadConvertor defines the interface to convert system payload to webhook payload
+type payloadConvertor[T any] interface {
+	Create(*api.CreatePayload) (T, error)
+	Delete(*api.DeletePayload) (T, error)
+	Fork(*api.ForkPayload) (T, error)
+	Issue(*api.IssuePayload) (T, error)
+	IssueComment(*api.IssueCommentPayload) (T, error)
+	Push(*api.PushPayload) (T, error)
+	PullRequest(*api.PullRequestPayload) (T, error)
+	Review(*api.PullRequestPayload, webhook_module.HookEventType) (T, error)
+	Repository(*api.RepositoryPayload) (T, error)
+	Release(*api.ReleasePayload) (T, error)
+	Wiki(*api.WikiPayload) (T, error)
+	Package(*api.PackagePayload) (T, error)
 }
 
-func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) {
+func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) {
+	var p P
+	if err := json.Unmarshal(data, &p); err != nil {
+		var t T
+		return t, fmt.Errorf("could not unmarshal payload: %w", err)
+	}
+	return convert(p)
+}
+
+func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) {
 	switch event {
 	case webhook_module.HookEventCreate:
-		return s.Create(p.(*api.CreatePayload))
+		return convertUnmarshalledJSON(rc.Create, data)
 	case webhook_module.HookEventDelete:
-		return s.Delete(p.(*api.DeletePayload))
+		return convertUnmarshalledJSON(rc.Delete, data)
 	case webhook_module.HookEventFork:
-		return s.Fork(p.(*api.ForkPayload))
+		return convertUnmarshalledJSON(rc.Fork, data)
 	case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueMilestone:
-		return s.Issue(p.(*api.IssuePayload))
+		return convertUnmarshalledJSON(rc.Issue, data)
 	case webhook_module.HookEventIssueComment, webhook_module.HookEventPullRequestComment:
-		pl, ok := p.(*api.IssueCommentPayload)
-		if ok {
-			return s.IssueComment(pl)
-		}
-		return s.PullRequest(p.(*api.PullRequestPayload))
+		// previous code sometimes sent s.PullRequest(p.(*api.PullRequestPayload))
+		// however I couldn't find in notifier.go such a payload with an HookEvent***Comment event
+
+		// History (most recent first):
+		//  - refactored in https://github.com/go-gitea/gitea/pull/12310
+		//  - assertion added in https://github.com/go-gitea/gitea/pull/12046
+		//  - issue raised in https://github.com/go-gitea/gitea/issues/11940#issuecomment-645713996
+		//    > That's because for HookEventPullRequestComment event, some places use IssueCommentPayload and others use PullRequestPayload
+
+		// In modules/actions/workflows.go:183 the type assertion is always payload.(*api.IssueCommentPayload)
+		return convertUnmarshalledJSON(rc.IssueComment, data)
 	case webhook_module.HookEventPush:
-		return s.Push(p.(*api.PushPayload))
+		return convertUnmarshalledJSON(rc.Push, data)
 	case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel,
 		webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestReviewRequest:
-		return s.PullRequest(p.(*api.PullRequestPayload))
+		return convertUnmarshalledJSON(rc.PullRequest, data)
 	case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment:
-		return s.Review(p.(*api.PullRequestPayload), event)
+		return convertUnmarshalledJSON(func(p *api.PullRequestPayload) (T, error) {
+			return rc.Review(p, event)
+		}, data)
 	case webhook_module.HookEventRepository:
-		return s.Repository(p.(*api.RepositoryPayload))
+		return convertUnmarshalledJSON(rc.Repository, data)
 	case webhook_module.HookEventRelease:
-		return s.Release(p.(*api.ReleasePayload))
+		return convertUnmarshalledJSON(rc.Release, data)
 	case webhook_module.HookEventWiki:
-		return s.Wiki(p.(*api.WikiPayload))
+		return convertUnmarshalledJSON(rc.Wiki, data)
 	case webhook_module.HookEventPackage:
-		return s.Package(p.(*api.PackagePayload))
+		return convertUnmarshalledJSON(rc.Package, data)
 	}
-	return s, nil
+	var t T
+	return t, fmt.Errorf("newPayload unsupported event: %s", event)
+}
+
+func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
+	payload, err := newPayload(pc, []byte(t.PayloadContent), t.EventType)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	body, err := json.MarshalIndent(payload, "", "  ")
+	if err != nil {
+		return nil, nil, err
+	}
+
+	method := w.HTTPMethod
+	if method == "" {
+		method = http.MethodPost
+	}
+
+	req, err := http.NewRequest(method, w.URL, bytes.NewReader(body))
+	if err != nil {
+		return nil, nil, err
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	if withDefaultHeaders {
+		return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
+	}
+	return req, body, nil
 }
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index 945b0662d8..ba8bac27d9 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -4,8 +4,9 @@
 package webhook
 
 import (
-	"errors"
+	"context"
 	"fmt"
+	"net/http"
 	"regexp"
 	"strings"
 
@@ -39,7 +40,6 @@ func GetSlackHook(w *webhook_model.Webhook) *SlackMeta {
 type SlackPayload struct {
 	Channel     string            `json:"channel"`
 	Text        string            `json:"text"`
-	Color       string            `json:"-"`
 	Username    string            `json:"username"`
 	IconURL     string            `json:"icon_url"`
 	UnfurlLinks int               `json:"unfurl_links"`
@@ -56,15 +56,6 @@ type SlackAttachment struct {
 	Text      string `json:"text"`
 }
 
-// JSONPayload Marshals the SlackPayload to json
-func (s *SlackPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(s, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
 // SlackTextFormatter replaces &, <, > with HTML characters
 // see: https://api.slack.com/docs/formatting
 func SlackTextFormatter(s string) string {
@@ -98,10 +89,8 @@ func SlackLinkToRef(repoURL, ref string) string {
 	return SlackLinkFormatter(url, refName)
 }
 
-var _ PayloadConvertor = &SlackPayload{}
-
-// Create implements PayloadConvertor Create method
-func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+// Create implements payloadConvertor Create method
+func (s slackConvertor) Create(p *api.CreatePayload) (SlackPayload, error) {
 	repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
 	text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
@@ -110,7 +99,7 @@ func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
 }
 
 // Delete composes Slack payload for delete a branch or tag.
-func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (s slackConvertor) Delete(p *api.DeletePayload) (SlackPayload, error) {
 	refName := git.RefName(p.Ref).ShortName()
 	repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
@@ -119,7 +108,7 @@ func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
 }
 
 // Fork composes Slack payload for forked by a repository.
-func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (s slackConvertor) Fork(p *api.ForkPayload) (SlackPayload, error) {
 	baseLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
 	forkLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
 	text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
@@ -127,8 +116,8 @@ func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
 	return s.createPayload(text, nil), nil
 }
 
-// Issue implements PayloadConvertor Issue method
-func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+// Issue implements payloadConvertor Issue method
+func (s slackConvertor) Issue(p *api.IssuePayload) (SlackPayload, error) {
 	text, issueTitle, attachmentText, color := getIssuesPayloadInfo(p, SlackLinkFormatter, true)
 
 	var attachments []SlackAttachment
@@ -146,8 +135,8 @@ func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
 	return s.createPayload(text, attachments), nil
 }
 
-// IssueComment implements PayloadConvertor IssueComment method
-func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+// IssueComment implements payloadConvertor IssueComment method
+func (s slackConvertor) IssueComment(p *api.IssueCommentPayload) (SlackPayload, error) {
 	text, issueTitle, color := getIssueCommentPayloadInfo(p, SlackLinkFormatter, true)
 
 	return s.createPayload(text, []SlackAttachment{{
@@ -158,28 +147,28 @@ func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader,
 	}}), nil
 }
 
-// Wiki implements PayloadConvertor Wiki method
-func (s *SlackPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+// Wiki implements payloadConvertor Wiki method
+func (s slackConvertor) Wiki(p *api.WikiPayload) (SlackPayload, error) {
 	text, _, _ := getWikiPayloadInfo(p, SlackLinkFormatter, true)
 
 	return s.createPayload(text, nil), nil
 }
 
-// Release implements PayloadConvertor Release method
-func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+// Release implements payloadConvertor Release method
+func (s slackConvertor) Release(p *api.ReleasePayload) (SlackPayload, error) {
 	text, _ := getReleasePayloadInfo(p, SlackLinkFormatter, true)
 
 	return s.createPayload(text, nil), nil
 }
 
-func (s *SlackPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) {
 	text, _ := getPackagePayloadInfo(p, SlackLinkFormatter, true)
 
 	return s.createPayload(text, nil), nil
 }
 
-// Push implements PayloadConvertor Push method
-func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+// Push implements payloadConvertor Push method
+func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
 	// n new commits
 	var (
 		commitDesc   string
@@ -219,8 +208,8 @@ func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 	}}), nil
 }
 
-// PullRequest implements PayloadConvertor PullRequest method
-func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+// PullRequest implements payloadConvertor PullRequest method
+func (s slackConvertor) PullRequest(p *api.PullRequestPayload) (SlackPayload, error) {
 	text, issueTitle, attachmentText, color := getPullRequestPayloadInfo(p, SlackLinkFormatter, true)
 
 	var attachments []SlackAttachment
@@ -238,8 +227,8 @@ func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, er
 	return s.createPayload(text, attachments), nil
 }
 
-// Review implements PayloadConvertor Review method
-func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+// Review implements payloadConvertor Review method
+func (s slackConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (SlackPayload, error) {
 	senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
 	title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
 	titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index)
@@ -250,7 +239,7 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return SlackPayload{}, err
 		}
 
 		text = fmt.Sprintf("[%s] Pull request review %s: [%s](%s) by %s", repoLink, action, title, titleLink, senderLink)
@@ -259,8 +248,8 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho
 	return s.createPayload(text, nil), nil
 }
 
-// Repository implements PayloadConvertor Repository method
-func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+// Repository implements payloadConvertor Repository method
+func (s slackConvertor) Repository(p *api.RepositoryPayload) (SlackPayload, error) {
 	senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
 	repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
 	var text string
@@ -275,8 +264,8 @@ func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, erro
 	return s.createPayload(text, nil), nil
 }
 
-func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment) *SlackPayload {
-	return &SlackPayload{
+func (s slackConvertor) createPayload(text string, attachments []SlackAttachment) SlackPayload {
+	return SlackPayload{
 		Channel:     s.Channel,
 		Text:        text,
 		Username:    s.Username,
@@ -285,21 +274,27 @@ func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment)
 	}
 }
 
-// GetSlackPayload converts a slack webhook into a SlackPayload
-func GetSlackPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
-	s := new(SlackPayload)
+type slackConvertor struct {
+	Channel  string
+	Username string
+	IconURL  string
+	Color    string
+}
 
-	slack := &SlackMeta{}
-	if err := json.Unmarshal([]byte(meta), &slack); err != nil {
-		return s, errors.New("GetSlackPayload meta json:" + err.Error())
+var _ payloadConvertor[SlackPayload] = slackConvertor{}
+
+func newSlackRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	meta := &SlackMeta{}
+	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
+		return nil, nil, fmt.Errorf("newSlackRequest meta json: %w", err)
 	}
-
-	s.Channel = slack.Channel
-	s.Username = slack.Username
-	s.IconURL = slack.IconURL
-	s.Color = slack.Color
-
-	return convertPayloader(s, p, event)
+	sc := slackConvertor{
+		Channel:  meta.Channel,
+		Username: meta.Username,
+		IconURL:  meta.IconURL,
+		Color:    meta.Color,
+	}
+	return newJSONRequest(sc, w, t, true)
 }
 
 var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`)
diff --git a/services/webhook/slack_test.go b/services/webhook/slack_test.go
index b1340963e2..7ebf16aba2 100644
--- a/services/webhook/slack_test.go
+++ b/services/webhook/slack_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -14,201 +17,180 @@ import (
 )
 
 func TestSlackPayload(t *testing.T) {
+	sc := slackConvertor{}
+
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Create(p)
+		pl, err := sc.Create(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] branch created by user1", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] branch created by user1", pl.Text)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Delete(p)
+		pl, err := sc.Delete(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:test] branch deleted by user1", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:test] branch deleted by user1", pl.Text)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Fork(p)
+		pl, err := sc.Fork(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "<http://localhost:3000/test/repo2|test/repo2> is forked to <http://localhost:3000/test/repo|test/repo>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "<http://localhost:3000/test/repo2|test/repo2> is forked to <http://localhost:3000/test/repo|test/repo>", pl.Text)
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Push(p)
+		pl, err := sc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] 2 new commits pushed by user1", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] 2 new commits pushed by user1", pl.Text)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(SlackPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := sc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Issue opened: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Issue opened: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.Text)
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = sc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Issue closed: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Issue closed: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := sc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New comment on issue <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New comment on issue <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := sc.PullRequest(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Pull request opened: <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Pull request opened: <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := sc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New comment on pull request <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New comment on pull request <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(SlackPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := sc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Repository(p)
+		pl, err := sc.Repository(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Repository created by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Repository created by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Package(p)
+		pl, err := sc.Package(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "Package created: <http://localhost:3000/user1/-/packages/container/GiteaContainer/latest|GiteaContainer:latest> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "Package created: <http://localhost:3000/user1/-/packages/container/GiteaContainer/latest|GiteaContainer:latest> by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(SlackPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := sc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New wiki page '<http://localhost:3000/test/repo/wiki/index|index>' (Wiki change comment) by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New wiki page '<http://localhost:3000/test/repo/wiki/index|index>' (Wiki change comment) by <https://try.gitea.io/user1|user1>", pl.Text)
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = sc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' edited (Wiki change comment) by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' edited (Wiki change comment) by <https://try.gitea.io/user1|user1>", pl.Text)
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = sc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' deleted by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' deleted by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(SlackPayload)
-		pl, err := d.Release(p)
+		pl, err := sc.Release(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &SlackPayload{}, pl)
 
-		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Release created: <http://localhost:3000/test/repo/releases/tag/v1.0|v1.0> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text)
+		assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Release created: <http://localhost:3000/test/repo/releases/tag/v1.0|v1.0> by <https://try.gitea.io/user1|user1>", pl.Text)
 	})
 }
 
 func TestSlackJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(SlackPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &SlackPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.SLACK,
+		URL:        "https://slack.example.com/",
+		Meta:       `{}`,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newSlackRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://slack.example.com/", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body SlackPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] 2 new commits pushed by user1", body.Text)
 }
 
 func TestIsValidSlackChannel(t *testing.T) {
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index 1bdc74e183..c2b4820032 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -4,14 +4,15 @@
 package webhook
 
 import (
+	"context"
 	"fmt"
+	"net/http"
 	"strings"
 
 	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/markup"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 )
@@ -41,22 +42,8 @@ func GetTelegramHook(w *webhook_model.Webhook) *TelegramMeta {
 	return s
 }
 
-var _ PayloadConvertor = &TelegramPayload{}
-
-// JSONPayload Marshals the TelegramPayload to json
-func (t *TelegramPayload) JSONPayload() ([]byte, error) {
-	t.ParseMode = "HTML"
-	t.DisableWebPreview = true
-	t.Message = markup.Sanitize(t.Message)
-	data, err := json.MarshalIndent(t, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
 // Create implements PayloadConvertor Create method
-func (t *TelegramPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (t telegramConvertor) Create(p *api.CreatePayload) (TelegramPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf(`[<a href="%s">%s</a>] %s <a href="%s">%s</a> created`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType,
@@ -66,7 +53,7 @@ func (t *TelegramPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
 }
 
 // Delete implements PayloadConvertor Delete method
-func (t *TelegramPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (t telegramConvertor) Delete(p *api.DeletePayload) (TelegramPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf(`[<a href="%s">%s</a>] %s <a href="%s">%s</a> deleted`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType,
@@ -76,14 +63,14 @@ func (t *TelegramPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
 }
 
 // Fork implements PayloadConvertor Fork method
-func (t *TelegramPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (t telegramConvertor) Fork(p *api.ForkPayload) (TelegramPayload, error) {
 	title := fmt.Sprintf(`%s is forked to <a href="%s">%s</a>`, p.Forkee.FullName, p.Repo.HTMLURL, p.Repo.FullName)
 
 	return createTelegramPayload(title), nil
 }
 
 // Push implements PayloadConvertor Push method
-func (t *TelegramPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (t telegramConvertor) Push(p *api.PushPayload) (TelegramPayload, error) {
 	var (
 		branchName = git.RefName(p.Ref).ShortName()
 		commitDesc string
@@ -121,34 +108,34 @@ func (t *TelegramPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 }
 
 // Issue implements PayloadConvertor Issue method
-func (t *TelegramPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (t telegramConvertor) Issue(p *api.IssuePayload) (TelegramPayload, error) {
 	text, _, attachmentText, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true)
 
 	return createTelegramPayload(text + "\n\n" + attachmentText), nil
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (t *TelegramPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (t telegramConvertor) IssueComment(p *api.IssueCommentPayload) (TelegramPayload, error) {
 	text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true)
 
 	return createTelegramPayload(text + "\n" + p.Comment.Body), nil
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (t *TelegramPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (t telegramConvertor) PullRequest(p *api.PullRequestPayload) (TelegramPayload, error) {
 	text, _, attachmentText, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true)
 
 	return createTelegramPayload(text + "\n" + attachmentText), nil
 }
 
 // Review implements PayloadConvertor Review method
-func (t *TelegramPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (t telegramConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (TelegramPayload, error) {
 	var text, attachmentText string
 	switch p.Action {
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return TelegramPayload{}, err
 		}
 
 		text = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -159,7 +146,7 @@ func (t *TelegramPayload) Review(p *api.PullRequestPayload, event webhook_module
 }
 
 // Repository implements PayloadConvertor Repository method
-func (t *TelegramPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (t telegramConvertor) Repository(p *api.RepositoryPayload) (TelegramPayload, error) {
 	var title string
 	switch p.Action {
 	case api.HookRepoCreated:
@@ -169,36 +156,41 @@ func (t *TelegramPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e
 		title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
 		return createTelegramPayload(title), nil
 	}
-	return nil, nil
+	return TelegramPayload{}, nil
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (t *TelegramPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (t telegramConvertor) Wiki(p *api.WikiPayload) (TelegramPayload, error) {
 	text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true)
 
 	return createTelegramPayload(text), nil
 }
 
 // Release implements PayloadConvertor Release method
-func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (t telegramConvertor) Release(p *api.ReleasePayload) (TelegramPayload, error) {
 	text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true)
 
 	return createTelegramPayload(text), nil
 }
 
-func (t *TelegramPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, error) {
 	text, _ := getPackagePayloadInfo(p, htmlLinkFormatter, true)
 
 	return createTelegramPayload(text), nil
 }
 
-// GetTelegramPayload converts a telegram webhook into a TelegramPayload
-func GetTelegramPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
-	return convertPayloader(new(TelegramPayload), p, event)
-}
-
-func createTelegramPayload(message string) *TelegramPayload {
-	return &TelegramPayload{
-		Message: strings.TrimSpace(message),
+func createTelegramPayload(message string) TelegramPayload {
+	return TelegramPayload{
+		Message:           strings.TrimSpace(message),
+		ParseMode:         "HTML",
+		DisableWebPreview: true,
 	}
 }
+
+type telegramConvertor struct{}
+
+var _ payloadConvertor[TelegramPayload] = telegramConvertor{}
+
+func newTelegramRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	return newJSONRequest(telegramConvertor{}, w, t, true)
+}
diff --git a/services/webhook/telegram_test.go b/services/webhook/telegram_test.go
index 5b9927d057..2fe5161b22 100644
--- a/services/webhook/telegram_test.go
+++ b/services/webhook/telegram_test.go
@@ -4,8 +4,11 @@
 package webhook
 
 import (
+	"context"
 	"testing"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
@@ -14,199 +17,186 @@ import (
 )
 
 func TestTelegramPayload(t *testing.T) {
+	tc := telegramConvertor{}
+
+	t.Run("Correct webhook params", func(t *testing.T) {
+		p := createTelegramPayload("testMsg ")
+
+		assert.Equal(t, "HTML", p.ParseMode)
+		assert.Equal(t, true, p.DisableWebPreview)
+		assert.Equal(t, "testMsg", p.Message)
+	})
+
 	t.Run("Create", func(t *testing.T) {
 		p := createTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Create(p)
+		pl, err := tc.Create(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test">test</a> created`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test">test</a> created`, pl.Message)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		p := deleteTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Delete(p)
+		pl, err := tc.Delete(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test">test</a> deleted`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test">test</a> deleted`, pl.Message)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
 		p := forkTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Fork(p)
+		pl, err := tc.Fork(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `test/repo2 is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `test/repo2 is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, pl.Message)
 	})
 
 	t.Run("Push", func(t *testing.T) {
 		p := pushTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Push(p)
+		pl, err := tc.Push(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>:<a href=\"http://localhost:3000/test/repo/src/test\">test</a>] 2 new commits\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1", pl.(*TelegramPayload).Message)
+		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>:<a href=\"http://localhost:3000/test/repo/src/test\">test</a>] 2 new commits\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1", pl.Message)
 	})
 
 	t.Run("Issue", func(t *testing.T) {
 		p := issueTestPayload()
 
-		d := new(TelegramPayload)
 		p.Action = api.HookIssueOpened
-		pl, err := d.Issue(p)
+		pl, err := tc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] Issue opened: <a href=\"http://localhost:3000/test/repo/issues/2\">#2 crash</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\n\nissue body", pl.(*TelegramPayload).Message)
+		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] Issue opened: <a href=\"http://localhost:3000/test/repo/issues/2\">#2 crash</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\n\nissue body", pl.Message)
 
 		p.Action = api.HookIssueClosed
-		pl, err = d.Issue(p)
+		pl, err = tc.Issue(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.Message)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
 		p := issueCommentTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := tc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] New comment on issue <a href=\"http://localhost:3000/test/repo/issues/2\">#2 crash</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nmore info needed", pl.(*TelegramPayload).Message)
+		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] New comment on issue <a href=\"http://localhost:3000/test/repo/issues/2\">#2 crash</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nmore info needed", pl.Message)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
 		p := pullRequestTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.PullRequest(p)
+		pl, err := tc.PullRequest(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] Pull request opened: <a href=\"http://localhost:3000/test/repo/pulls/12\">#12 Fix bug</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nfixes bug #2", pl.(*TelegramPayload).Message)
+		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] Pull request opened: <a href=\"http://localhost:3000/test/repo/pulls/12\">#12 Fix bug</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nfixes bug #2", pl.Message)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
 		p := pullRequestCommentTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.IssueComment(p)
+		pl, err := tc.IssueComment(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] New comment on pull request <a href=\"http://localhost:3000/test/repo/pulls/12\">#12 Fix bug</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nchanges requested", pl.(*TelegramPayload).Message)
+		assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] New comment on pull request <a href=\"http://localhost:3000/test/repo/pulls/12\">#12 Fix bug</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nchanges requested", pl.Message)
 	})
 
 	t.Run("Review", func(t *testing.T) {
 		p := pullRequestTestPayload()
 		p.Action = api.HookIssueReviewed
 
-		d := new(TelegramPayload)
-		pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+		pl, err := tc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug\ngood job", pl.(*TelegramPayload).Message)
+		assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug\ngood job", pl.Message)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
 		p := repositoryTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Repository(p)
+		pl, err := tc.Repository(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created`, pl.Message)
 	})
 
 	t.Run("Package", func(t *testing.T) {
 		p := packageTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Package(p)
+		pl, err := tc.Package(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `Package created: <a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer:latest</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `Package created: <a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer:latest</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.Message)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
 		p := wikiTestPayload()
 
-		d := new(TelegramPayload)
 		p.Action = api.HookWikiCreated
-		pl, err := d.Wiki(p)
+		pl, err := tc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.Message)
 
 		p.Action = api.HookWikiEdited
-		pl, err = d.Wiki(p)
+		pl, err = tc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.Message)
 
 		p.Action = api.HookWikiDeleted
-		pl, err = d.Wiki(p)
+		pl, err = tc.Wiki(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, pl.Message)
 	})
 
 	t.Run("Release", func(t *testing.T) {
 		p := pullReleaseTestPayload()
 
-		d := new(TelegramPayload)
-		pl, err := d.Release(p)
+		pl, err := tc.Release(p)
 		require.NoError(t, err)
-		require.NotNil(t, pl)
-		require.IsType(t, &TelegramPayload{}, pl)
 
-		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message)
+		assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.Message)
 	})
 }
 
 func TestTelegramJSONPayload(t *testing.T) {
 	p := pushTestPayload()
-
-	pl, err := new(TelegramPayload).Push(p)
+	data, err := p.JSONPayload()
 	require.NoError(t, err)
-	require.NotNil(t, pl)
-	require.IsType(t, &TelegramPayload{}, pl)
 
-	json, err := pl.JSONPayload()
+	hook := &webhook_model.Webhook{
+		RepoID:     3,
+		IsActive:   true,
+		Type:       webhook_module.TELEGRAM,
+		URL:        "https://telegram.example.com/",
+		Meta:       ``,
+		HTTPMethod: "POST",
+	}
+	task := &webhook_model.HookTask{
+		HookID:         hook.ID,
+		EventType:      webhook_module.HookEventPush,
+		PayloadContent: string(data),
+		PayloadVersion: 2,
+	}
+
+	req, reqBody, err := newTelegramRequest(context.Background(), hook, task)
+	require.NotNil(t, req)
+	require.NotNil(t, reqBody)
 	require.NoError(t, err)
-	assert.NotEmpty(t, json)
+
+	assert.Equal(t, "POST", req.Method)
+	assert.Equal(t, "https://telegram.example.com/", req.URL.String())
+	assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+	assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+	var body TelegramPayload
+	err = json.NewDecoder(req.Body).Decode(&body)
+	assert.NoError(t, err)
+	assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>:<a href=\"http://localhost:3000/test/repo/src/test\">test</a>] 2 new commits\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1", body.Message)
 }
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index ac18da3525..e0e8fa2fc1 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"net/http"
 	"strings"
 
 	"code.gitea.io/gitea/models/db"
@@ -16,6 +17,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/queue"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
@@ -25,48 +27,16 @@ import (
 	"github.com/gobwas/glob"
 )
 
-type webhook struct {
-	name           webhook_module.HookType
-	payloadCreator func(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error)
-}
-
-var webhooks = map[webhook_module.HookType]*webhook{
-	webhook_module.SLACK: {
-		name:           webhook_module.SLACK,
-		payloadCreator: GetSlackPayload,
-	},
-	webhook_module.DISCORD: {
-		name:           webhook_module.DISCORD,
-		payloadCreator: GetDiscordPayload,
-	},
-	webhook_module.DINGTALK: {
-		name:           webhook_module.DINGTALK,
-		payloadCreator: GetDingtalkPayload,
-	},
-	webhook_module.TELEGRAM: {
-		name:           webhook_module.TELEGRAM,
-		payloadCreator: GetTelegramPayload,
-	},
-	webhook_module.MSTEAMS: {
-		name:           webhook_module.MSTEAMS,
-		payloadCreator: GetMSTeamsPayload,
-	},
-	webhook_module.FEISHU: {
-		name:           webhook_module.FEISHU,
-		payloadCreator: GetFeishuPayload,
-	},
-	webhook_module.MATRIX: {
-		name:           webhook_module.MATRIX,
-		payloadCreator: GetMatrixPayload,
-	},
-	webhook_module.WECHATWORK: {
-		name:           webhook_module.WECHATWORK,
-		payloadCreator: GetWechatworkPayload,
-	},
-	webhook_module.PACKAGIST: {
-		name:           webhook_module.PACKAGIST,
-		payloadCreator: GetPackagistPayload,
-	},
+var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){
+	webhook_module.SLACK:      newSlackRequest,
+	webhook_module.DISCORD:    newDiscordRequest,
+	webhook_module.DINGTALK:   newDingtalkRequest,
+	webhook_module.TELEGRAM:   newTelegramRequest,
+	webhook_module.MSTEAMS:    newMSTeamsRequest,
+	webhook_module.FEISHU:     newFeishuRequest,
+	webhook_module.MATRIX:     newMatrixRequest,
+	webhook_module.WECHATWORK: newWechatworkRequest,
+	webhook_module.PACKAGIST:  newPackagistRequest,
 }
 
 // IsValidHookTaskType returns true if a webhook registered
@@ -74,7 +44,7 @@ func IsValidHookTaskType(name string) bool {
 	if name == webhook_module.GITEA || name == webhook_module.GOGS {
 		return true
 	}
-	_, ok := webhooks[name]
+	_, ok := webhookRequesters[name]
 	return ok
 }
 
@@ -158,7 +128,9 @@ func checkBranch(w *webhook_model.Webhook, branch string) bool {
 	return g.Match(branch)
 }
 
-// PrepareWebhook creates a hook task and enqueues it for processing
+// PrepareWebhook creates a hook task and enqueues it for processing.
+// The payload is saved as-is. The adjustments depending on the webhook type happen
+// right before delivery, in the [Deliver] method.
 func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error {
 	// Skip sending if webhooks are disabled.
 	if setting.DisableWebhooks {
@@ -192,25 +164,19 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook
 		}
 	}
 
-	var payloader api.Payloader
-	var err error
-	webhook, ok := webhooks[w.Type]
-	if ok {
-		payloader, err = webhook.payloadCreator(p, event, w.Meta)
-		if err != nil {
-			return fmt.Errorf("create payload for %s[%s]: %w", w.Type, event, err)
-		}
-	} else {
-		payloader = p
+	payload, err := p.JSONPayload()
+	if err != nil {
+		return fmt.Errorf("JSONPayload for %s: %w", event, err)
 	}
 
 	task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{
-		HookID:    w.ID,
-		Payloader: payloader,
-		EventType: event,
+		HookID:         w.ID,
+		PayloadContent: string(payload),
+		EventType:      event,
+		PayloadVersion: 2,
 	})
 	if err != nil {
-		return fmt.Errorf("CreateHookTask: %w", err)
+		return fmt.Errorf("CreateHookTask for %s: %w", event, err)
 	}
 
 	return enqueueHookTask(task.ID)
@@ -225,7 +191,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu
 	if source.Repository != nil {
 		repoHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
 			RepoID:   source.Repository.ID,
-			IsActive: util.OptionalBoolTrue,
+			IsActive: optional.Some(true),
 		})
 		if err != nil {
 			return fmt.Errorf("ListWebhooksByOpts: %w", err)
@@ -239,7 +205,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu
 	if owner != nil {
 		ownerHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
 			OwnerID:  owner.ID,
-			IsActive: util.OptionalBoolTrue,
+			IsActive: optional.Some(true),
 		})
 		if err != nil {
 			return fmt.Errorf("ListWebhooksByOpts: %w", err)
@@ -248,7 +214,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu
 	}
 
 	// Add any admin-defined system webhooks
-	systemHooks, err := webhook_model.GetSystemWebhooks(ctx, util.OptionalBoolTrue)
+	systemHooks, err := webhook_model.GetSystemWebhooks(ctx, optional.Some(true))
 	if err != nil {
 		return fmt.Errorf("GetSystemWebhooks: %w", err)
 	}
diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go
index 338b94360b..5f5c146232 100644
--- a/services/webhook/webhook_test.go
+++ b/services/webhook/webhook_test.go
@@ -77,7 +77,3 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) {
 		unittest.AssertNotExistsBean(t, hookTask)
 	}
 }
-
-// TODO TestHookTask_deliver
-
-// TODO TestDeliverHooks
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 80245c7e77..46e7856ecf 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -4,11 +4,13 @@
 package webhook
 
 import (
+	"context"
 	"fmt"
+	"net/http"
 	"strings"
 
+	webhook_model "code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 )
@@ -28,20 +30,8 @@ type (
 	}
 )
 
-// SetSecret sets the Wechatwork secret
-func (f *WechatworkPayload) SetSecret(_ string) {}
-
-// JSONPayload Marshals the WechatworkPayload to json
-func (f *WechatworkPayload) JSONPayload() ([]byte, error) {
-	data, err := json.MarshalIndent(f, "", "  ")
-	if err != nil {
-		return []byte{}, err
-	}
-	return data, nil
-}
-
-func newWechatworkMarkdownPayload(title string) *WechatworkPayload {
-	return &WechatworkPayload{
+func newWechatworkMarkdownPayload(title string) WechatworkPayload {
+	return WechatworkPayload{
 		Msgtype: "markdown",
 		Markdown: struct {
 			Content string `json:"content"`
@@ -51,10 +41,8 @@ func newWechatworkMarkdownPayload(title string) *WechatworkPayload {
 	}
 }
 
-var _ PayloadConvertor = &WechatworkPayload{}
-
 // Create implements PayloadConvertor Create method
-func (f *WechatworkPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Create(p *api.CreatePayload) (WechatworkPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -63,7 +51,7 @@ func (f *WechatworkPayload) Create(p *api.CreatePayload) (api.Payloader, error)
 }
 
 // Delete implements PayloadConvertor Delete method
-func (f *WechatworkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Delete(p *api.DeletePayload) (WechatworkPayload, error) {
 	// created tag/branch
 	refName := git.RefName(p.Ref).ShortName()
 	title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -72,14 +60,14 @@ func (f *WechatworkPayload) Delete(p *api.DeletePayload) (api.Payloader, error)
 }
 
 // Fork implements PayloadConvertor Fork method
-func (f *WechatworkPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Fork(p *api.ForkPayload) (WechatworkPayload, error) {
 	title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
 
 	return newWechatworkMarkdownPayload(title), nil
 }
 
 // Push implements PayloadConvertor Push method
-func (f *WechatworkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error) {
 	var (
 		branchName = git.RefName(p.Ref).ShortName()
 		commitDesc string
@@ -108,7 +96,7 @@ func (f *WechatworkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
 }
 
 // Issue implements PayloadConvertor Issue method
-func (f *WechatworkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Issue(p *api.IssuePayload) (WechatworkPayload, error) {
 	text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true)
 	var content string
 	content += fmt.Sprintf(" ><font color=\"info\">%s</font>\n >%s \n ><font color=\"warning\"> %s</font> \n [%s](%s)", text, attachmentText, issueTitle, p.Issue.HTMLURL, p.Issue.HTMLURL)
@@ -117,7 +105,7 @@ func (f *WechatworkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
 }
 
 // IssueComment implements PayloadConvertor IssueComment method
-func (f *WechatworkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) IssueComment(p *api.IssueCommentPayload) (WechatworkPayload, error) {
 	text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)
 	var content string
 	content += fmt.Sprintf(" ><font color=\"info\">%s</font>\n >%s \n ><font color=\"warning\">%s</font> \n [%s](%s)", text, p.Comment.Body, issueTitle, p.Comment.HTMLURL, p.Comment.HTMLURL)
@@ -126,7 +114,7 @@ func (f *WechatworkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloa
 }
 
 // PullRequest implements PayloadConvertor PullRequest method
-func (f *WechatworkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) PullRequest(p *api.PullRequestPayload) (WechatworkPayload, error) {
 	text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true)
 	pr := fmt.Sprintf("> <font color=\"info\"> %s </font> \r\n > <font color=\"comment\">%s </font> \r\n > <font color=\"comment\">%s </font> \r\n",
 		text, issueTitle, attachmentText)
@@ -135,13 +123,13 @@ func (f *WechatworkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloade
 }
 
 // Review implements PayloadConvertor Review method
-func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (wc wechatworkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (WechatworkPayload, error) {
 	var text, title string
 	switch p.Action {
 	case api.HookIssueReviewed:
 		action, err := parseHookPullRequestEventType(event)
 		if err != nil {
-			return nil, err
+			return WechatworkPayload{}, err
 		}
 		title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
 		text = p.Review.Content
@@ -151,7 +139,7 @@ func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_modu
 }
 
 // Repository implements PayloadConvertor Repository method
-func (f *WechatworkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Repository(p *api.RepositoryPayload) (WechatworkPayload, error) {
 	var title string
 	switch p.Action {
 	case api.HookRepoCreated:
@@ -162,30 +150,33 @@ func (f *WechatworkPayload) Repository(p *api.RepositoryPayload) (api.Payloader,
 		return newWechatworkMarkdownPayload(title), nil
 	}
 
-	return nil, nil
+	return WechatworkPayload{}, nil
 }
 
 // Wiki implements PayloadConvertor Wiki method
-func (f *WechatworkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Wiki(p *api.WikiPayload) (WechatworkPayload, error) {
 	text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
 
 	return newWechatworkMarkdownPayload(text), nil
 }
 
 // Release implements PayloadConvertor Release method
-func (f *WechatworkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Release(p *api.ReleasePayload) (WechatworkPayload, error) {
 	text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
 
 	return newWechatworkMarkdownPayload(text), nil
 }
 
-func (f *WechatworkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload, error) {
 	text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
 
 	return newWechatworkMarkdownPayload(text), nil
 }
 
-// GetWechatworkPayload GetWechatworkPayload converts a ding talk webhook into a WechatworkPayload
-func GetWechatworkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
-	return convertPayloader(new(WechatworkPayload), p, event)
+type wechatworkConvertor struct{}
+
+var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
+
+func newWechatworkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+	return newJSONRequest(wechatworkConvertor{}, w, t, true)
 }
diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go
index 6392d4ce83..8221e297d9 100644
--- a/services/wiki/wiki.go
+++ b/services/wiki/wiki.go
@@ -6,10 +6,12 @@ package wiki
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"os"
 	"strings"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	system_model "code.gitea.io/gitea/models/system"
 	"code.gitea.io/gitea/models/unit"
@@ -19,6 +21,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/sync"
+	"code.gitea.io/gitea/modules/util"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
@@ -26,10 +29,7 @@ import (
 // TODO: use clustered lock (unique queue? or *abuse* cache)
 var wikiWorkingPool = sync.NewExclusivePool()
 
-const (
-	DefaultRemote = "origin"
-	DefaultBranch = "master"
-)
+const DefaultRemote = "origin"
 
 // InitWiki initializes a wiki for repository,
 // it does nothing when repository already has wiki.
@@ -42,25 +42,25 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error {
 		return fmt.Errorf("InitRepository: %w", err)
 	} else if err = gitrepo.CreateDelegateHooks(ctx, repo, true); err != nil {
 		return fmt.Errorf("createDelegateHooks: %w", err)
-	} else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD", git.BranchPrefix+DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil {
-		return fmt.Errorf("unable to set default wiki branch to master: %w", err)
+	} else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix + repo.DefaultWikiBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil {
+		return fmt.Errorf("unable to set default wiki branch to %q: %w", repo.DefaultWikiBranch, err)
 	}
 	return nil
 }
 
 // prepareGitPath try to find a suitable file path with file name by the given raw wiki name.
 // return: existence, prepared file path with name, error
-func prepareGitPath(gitRepo *git.Repository, wikiPath WebPath) (bool, string, error) {
+func prepareGitPath(gitRepo *git.Repository, defaultWikiBranch string, wikiPath WebPath) (bool, string, error) {
 	unescaped := string(wikiPath) + ".md"
 	gitPath := WebPathToGitPath(wikiPath)
 
 	// Look for both files
-	filesInIndex, err := gitRepo.LsTree(DefaultBranch, unescaped, gitPath)
+	filesInIndex, err := gitRepo.LsTree(defaultWikiBranch, unescaped, gitPath)
 	if err != nil {
-		if strings.Contains(err.Error(), "Not a valid object name master") {
-			return false, gitPath, nil
+		if strings.Contains(err.Error(), "Not a valid object name") {
+			return false, gitPath, nil // branch doesn't exist
 		}
-		log.Error("%v", err)
+		log.Error("Wiki LsTree failed, err: %v", err)
 		return false, gitPath, err
 	}
 
@@ -96,7 +96,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 		return fmt.Errorf("InitWiki: %w", err)
 	}
 
-	hasMasterBranch := gitrepo.IsWikiBranchExist(ctx, repo, DefaultBranch)
+	hasDefaultBranch := gitrepo.IsWikiBranchExist(ctx, repo, repo.DefaultWikiBranch)
 
 	basePath, err := repo_module.CreateTemporaryPath("update-wiki")
 	if err != nil {
@@ -113,8 +113,8 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 		Shared: true,
 	}
 
-	if hasMasterBranch {
-		cloneOpts.Branch = DefaultBranch
+	if hasDefaultBranch {
+		cloneOpts.Branch = repo.DefaultWikiBranch
 	}
 
 	if err := git.Clone(ctx, repo.WikiPath(), basePath, cloneOpts); err != nil {
@@ -129,14 +129,14 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 	}
 	defer gitRepo.Close()
 
-	if hasMasterBranch {
+	if hasDefaultBranch {
 		if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
 			log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
 			return fmt.Errorf("fnable to read HEAD tree to index in: %s %w", basePath, err)
 		}
 	}
 
-	isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, newWikiName)
+	isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, newWikiName)
 	if err != nil {
 		return err
 	}
@@ -152,7 +152,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 		isOldWikiExist := true
 		oldWikiPath := newWikiPath
 		if oldWikiName != newWikiName {
-			isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, oldWikiName)
+			isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, repo.DefaultWikiBranch, oldWikiName)
 			if err != nil {
 				return err
 			}
@@ -161,7 +161,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 		if isOldWikiExist {
 			err := gitRepo.RemoveFilesFromIndex(oldWikiPath)
 			if err != nil {
-				log.Error("%v", err)
+				log.Error("RemoveFilesFromIndex failed: %v", err)
 				return err
 			}
 		}
@@ -171,18 +171,18 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 
 	objectHash, err := gitRepo.HashObject(strings.NewReader(content))
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("HashObject failed: %v", err)
 		return err
 	}
 
 	if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil {
-		log.Error("%v", err)
+		log.Error("AddObjectToIndex failed: %v", err)
 		return err
 	}
 
 	tree, err := gitRepo.WriteTree()
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("WriteTree failed: %v", err)
 		return err
 	}
 
@@ -201,19 +201,19 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 	} else {
 		commitTreeOpts.NoGPGSign = true
 	}
-	if hasMasterBranch {
+	if hasDefaultBranch {
 		commitTreeOpts.Parents = []string{"HEAD"}
 	}
 
 	commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts)
 	if err != nil {
-		log.Error("%v", err)
+		log.Error("CommitTree failed: %v", err)
 		return err
 	}
 
 	if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
 		Remote: DefaultRemote,
-		Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch),
+		Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch),
 		Env: repo_module.FullPushingEnvironment(
 			doer,
 			doer,
@@ -222,11 +222,11 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 			0,
 		),
 	}); err != nil {
-		log.Error("%v", err)
+		log.Error("Push failed: %v", err)
 		if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
 			return err
 		}
-		return fmt.Errorf("Push: %w", err)
+		return fmt.Errorf("failed to push: %w", err)
 	}
 
 	return nil
@@ -270,7 +270,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 	if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{
 		Bare:   true,
 		Shared: true,
-		Branch: DefaultBranch,
+		Branch: repo.DefaultWikiBranch,
 	}); err != nil {
 		log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
 		return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err)
@@ -288,7 +288,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 		return fmt.Errorf("unable to read HEAD tree to index in: %s %w", basePath, err)
 	}
 
-	found, wikiPath, err := prepareGitPath(gitRepo, wikiName)
+	found, wikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, wikiName)
 	if err != nil {
 		return err
 	}
@@ -332,7 +332,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 
 	if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
 		Remote: DefaultRemote,
-		Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch),
+		Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch),
 		Env: repo_module.FullPushingEnvironment(
 			doer,
 			doer,
@@ -359,3 +359,41 @@ func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error {
 	system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath())
 	return nil
 }
+
+func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, newBranch string) error {
+	if !git.IsValidRefPattern(newBranch) {
+		return fmt.Errorf("invalid branch name: %s", newBranch)
+	}
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		repo.DefaultWikiBranch = newBranch
+		if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_wiki_branch"); err != nil {
+			return fmt.Errorf("unable to update database: %w", err)
+		}
+
+		if !repo.HasWiki() {
+			return nil
+		}
+
+		oldDefBranch, err := gitrepo.GetWikiDefaultBranch(ctx, repo)
+		if err != nil {
+			return fmt.Errorf("unable to get default branch: %w", err)
+		}
+		if oldDefBranch == newBranch {
+			return nil
+		}
+
+		gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo)
+		if errors.Is(err, util.ErrNotExist) {
+			return nil // no git repo on storage, no need to do anything else
+		} else if err != nil {
+			return fmt.Errorf("unable to open repository: %w", err)
+		}
+		defer gitRepo.Close()
+
+		err = gitRepo.RenameBranch(oldDefBranch, newBranch)
+		if err != nil {
+			return fmt.Errorf("unable to rename default branch: %w", err)
+		}
+		return nil
+	})
+}
diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go
index 59c77060f2..0a18cffa25 100644
--- a/services/wiki/wiki_test.go
+++ b/services/wiki/wiki_test.go
@@ -170,7 +170,7 @@ func TestRepository_AddWikiPage(t *testing.T) {
 				return
 			}
 			defer gitRepo.Close()
-			masterTree, err := gitRepo.GetTree(DefaultBranch)
+			masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
 			assert.NoError(t, err)
 			gitPath := WebPathToGitPath(webPath)
 			entry, err := masterTree.GetTreeEntryByPath(gitPath)
@@ -215,7 +215,7 @@ func TestRepository_EditWikiPage(t *testing.T) {
 		// Now need to show that the page has been added:
 		gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo)
 		assert.NoError(t, err)
-		masterTree, err := gitRepo.GetTree(DefaultBranch)
+		masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
 		assert.NoError(t, err)
 		gitPath := WebPathToGitPath(webPath)
 		entry, err := masterTree.GetTreeEntryByPath(gitPath)
@@ -242,7 +242,7 @@ func TestRepository_DeleteWikiPage(t *testing.T) {
 		return
 	}
 	defer gitRepo.Close()
-	masterTree, err := gitRepo.GetTree(DefaultBranch)
+	masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
 	assert.NoError(t, err)
 	gitPath := WebPathToGitPath("Home")
 	_, err = masterTree.GetTreeEntryByPath(gitPath)
@@ -280,7 +280,7 @@ func TestPrepareWikiFileName(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			webPath := UserTitleToWebPath("", tt.arg)
-			existence, newWikiPath, err := prepareGitPath(gitRepo, webPath)
+			existence, newWikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, webPath)
 			if (err != nil) != tt.wantErr {
 				assert.NoError(t, err)
 				return
@@ -312,7 +312,7 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) {
 	}
 	defer gitRepo.Close()
 
-	existence, newWikiPath, err := prepareGitPath(gitRepo, "Home")
+	existence, newWikiPath, err := prepareGitPath(gitRepo, "master", "Home")
 	assert.False(t, existence)
 	assert.NoError(t, err)
 	assert.EqualValues(t, "Home.md", newWikiPath)
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 7c10074bc5..4c09a9d588 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -44,7 +44,7 @@ parts:
     source: .
     stage-packages: [ git, sqlite3, openssh-client ]
     build-packages: [ git, libpam0g-dev, libsqlite3-dev, build-essential]
-    build-snaps: [ go/1.21/stable, node/18/stable ]
+    build-snaps: [ go/1.22/stable, node/20/stable ]
     build-environment:
       - LDFLAGS: ""
     override-pull: |
diff --git a/stylelint.config.js b/stylelint.config.js
new file mode 100644
index 0000000000..523b18841e
--- /dev/null
+++ b/stylelint.config.js
@@ -0,0 +1,246 @@
+import {fileURLToPath} from 'node:url';
+
+const cssVarFiles = [
+  fileURLToPath(new URL('web_src/css/base.css', import.meta.url)),
+  fileURLToPath(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url)),
+  fileURLToPath(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url)),
+];
+
+/** @type {import('stylelint').Config} */
+export default {
+  plugins: [
+    'stylelint-declaration-strict-value',
+    'stylelint-declaration-block-no-ignored-properties',
+    'stylelint-value-no-unknown-custom-properties',
+    '@stylistic/stylelint-plugin',
+  ],
+  ignoreFiles: [
+    '**/*.go',
+    '/web_src/fomantic',
+  ],
+  overrides: [
+    {
+      files: ['**/chroma/*', '**/codemirror/*', '**/standalone/*', '**/console.css', 'font_i18n.css'],
+      rules: {
+        'scale-unlimited/declaration-strict-value': null,
+      },
+    },
+    {
+      files: ['**/chroma/*', '**/codemirror/*'],
+      rules: {
+        'block-no-empty': null,
+      },
+    },
+    {
+      files: ['**/*.vue'],
+      customSyntax: 'postcss-html',
+    },
+  ],
+  rules: {
+    '@stylistic/at-rule-name-case': null,
+    '@stylistic/at-rule-name-newline-after': null,
+    '@stylistic/at-rule-name-space-after': null,
+    '@stylistic/at-rule-semicolon-newline-after': null,
+    '@stylistic/at-rule-semicolon-space-before': null,
+    '@stylistic/block-closing-brace-empty-line-before': null,
+    '@stylistic/block-closing-brace-newline-after': null,
+    '@stylistic/block-closing-brace-newline-before': null,
+    '@stylistic/block-closing-brace-space-after': null,
+    '@stylistic/block-closing-brace-space-before': null,
+    '@stylistic/block-opening-brace-newline-after': null,
+    '@stylistic/block-opening-brace-newline-before': null,
+    '@stylistic/block-opening-brace-space-after': null,
+    '@stylistic/block-opening-brace-space-before': 'always',
+    '@stylistic/color-hex-case': 'lower',
+    '@stylistic/declaration-bang-space-after': 'never',
+    '@stylistic/declaration-bang-space-before': null,
+    '@stylistic/declaration-block-semicolon-newline-after': null,
+    '@stylistic/declaration-block-semicolon-newline-before': null,
+    '@stylistic/declaration-block-semicolon-space-after': null,
+    '@stylistic/declaration-block-semicolon-space-before': 'never',
+    '@stylistic/declaration-block-trailing-semicolon': null,
+    '@stylistic/declaration-colon-newline-after': null,
+    '@stylistic/declaration-colon-space-after': null,
+    '@stylistic/declaration-colon-space-before': 'never',
+    '@stylistic/function-comma-newline-after': null,
+    '@stylistic/function-comma-newline-before': null,
+    '@stylistic/function-comma-space-after': null,
+    '@stylistic/function-comma-space-before': null,
+    '@stylistic/function-max-empty-lines': 0,
+    '@stylistic/function-parentheses-newline-inside': 'never-multi-line',
+    '@stylistic/function-parentheses-space-inside': null,
+    '@stylistic/function-whitespace-after': null,
+    '@stylistic/indentation': 2,
+    '@stylistic/linebreaks': null,
+    '@stylistic/max-empty-lines': 1,
+    '@stylistic/max-line-length': null,
+    '@stylistic/media-feature-colon-space-after': null,
+    '@stylistic/media-feature-colon-space-before': 'never',
+    '@stylistic/media-feature-name-case': null,
+    '@stylistic/media-feature-parentheses-space-inside': null,
+    '@stylistic/media-feature-range-operator-space-after': 'always',
+    '@stylistic/media-feature-range-operator-space-before': 'always',
+    '@stylistic/media-query-list-comma-newline-after': null,
+    '@stylistic/media-query-list-comma-newline-before': null,
+    '@stylistic/media-query-list-comma-space-after': null,
+    '@stylistic/media-query-list-comma-space-before': null,
+    '@stylistic/named-grid-areas-alignment': null,
+    '@stylistic/no-empty-first-line': null,
+    '@stylistic/no-eol-whitespace': true,
+    '@stylistic/no-extra-semicolons': true,
+    '@stylistic/no-missing-end-of-source-newline': null,
+    '@stylistic/number-leading-zero': null,
+    '@stylistic/number-no-trailing-zeros': null,
+    '@stylistic/property-case': 'lower',
+    '@stylistic/selector-attribute-brackets-space-inside': null,
+    '@stylistic/selector-attribute-operator-space-after': null,
+    '@stylistic/selector-attribute-operator-space-before': null,
+    '@stylistic/selector-combinator-space-after': null,
+    '@stylistic/selector-combinator-space-before': null,
+    '@stylistic/selector-descendant-combinator-no-non-space': null,
+    '@stylistic/selector-list-comma-newline-after': null,
+    '@stylistic/selector-list-comma-newline-before': null,
+    '@stylistic/selector-list-comma-space-after': 'always-single-line',
+    '@stylistic/selector-list-comma-space-before': 'never-single-line',
+    '@stylistic/selector-max-empty-lines': 0,
+    '@stylistic/selector-pseudo-class-case': 'lower',
+    '@stylistic/selector-pseudo-class-parentheses-space-inside': 'never',
+    '@stylistic/selector-pseudo-element-case': 'lower',
+    '@stylistic/string-quotes': 'double',
+    '@stylistic/unicode-bom': null,
+    '@stylistic/unit-case': 'lower',
+    '@stylistic/value-list-comma-newline-after': null,
+    '@stylistic/value-list-comma-newline-before': null,
+    '@stylistic/value-list-comma-space-after': null,
+    '@stylistic/value-list-comma-space-before': null,
+    '@stylistic/value-list-max-empty-lines': 0,
+    'alpha-value-notation': null,
+    'annotation-no-unknown': true,
+    'at-rule-allowed-list': null,
+    'at-rule-disallowed-list': null,
+    'at-rule-empty-line-before': null,
+    'at-rule-no-unknown': [true, {ignoreAtRules: ['tailwind']}],
+    'at-rule-no-vendor-prefix': true,
+    'at-rule-property-required-list': null,
+    'block-no-empty': true,
+    'color-function-notation': null,
+    'color-hex-alpha': null,
+    'color-hex-length': null,
+    'color-named': null,
+    'color-no-hex': null,
+    'color-no-invalid-hex': true,
+    'comment-empty-line-before': null,
+    'comment-no-empty': true,
+    'comment-pattern': null,
+    'comment-whitespace-inside': null,
+    'comment-word-disallowed-list': null,
+    'csstools/value-no-unknown-custom-properties': [true, {importFrom: cssVarFiles}],
+    'custom-media-pattern': null,
+    'custom-property-empty-line-before': null,
+    'custom-property-no-missing-var-function': true,
+    'custom-property-pattern': null,
+    'declaration-block-no-duplicate-custom-properties': true,
+    'declaration-block-no-duplicate-properties': [true, {ignore: ['consecutive-duplicates-with-different-values']}],
+    'declaration-block-no-redundant-longhand-properties': null,
+    'declaration-block-no-shorthand-property-overrides': null,
+    'declaration-block-single-line-max-declarations': null,
+    'declaration-empty-line-before': null,
+    'declaration-no-important': null,
+    'declaration-property-max-values': null,
+    'declaration-property-unit-allowed-list': null,
+    'declaration-property-unit-disallowed-list': {'line-height': ['em']},
+    'declaration-property-value-allowed-list': null,
+    'declaration-property-value-disallowed-list': null,
+    'declaration-property-value-no-unknown': true,
+    'font-family-name-quotes': 'always-where-recommended',
+    'font-family-no-duplicate-names': true,
+    'font-family-no-missing-generic-family-keyword': true,
+    'font-weight-notation': null,
+    'function-allowed-list': null,
+    'function-calc-no-unspaced-operator': true,
+    'function-disallowed-list': null,
+    'function-linear-gradient-no-nonstandard-direction': true,
+    'function-name-case': 'lower',
+    'function-no-unknown': true,
+    'function-url-no-scheme-relative': null,
+    'function-url-quotes': 'always',
+    'function-url-scheme-allowed-list': null,
+    'function-url-scheme-disallowed-list': null,
+    'hue-degree-notation': null,
+    'import-notation': 'string',
+    'keyframe-block-no-duplicate-selectors': true,
+    'keyframe-declaration-no-important': true,
+    'keyframe-selector-notation': null,
+    'keyframes-name-pattern': null,
+    'length-zero-no-unit': [true, {ignore: ['custom-properties']}, {ignoreFunctions: ['var']}],
+    'max-nesting-depth': null,
+    'media-feature-name-allowed-list': null,
+    'media-feature-name-disallowed-list': null,
+    'media-feature-name-no-unknown': true,
+    'media-feature-name-no-vendor-prefix': true,
+    'media-feature-name-unit-allowed-list': null,
+    'media-feature-name-value-allowed-list': null,
+    'media-feature-name-value-no-unknown': true,
+    'media-feature-range-notation': null,
+    'media-query-no-invalid': true,
+    'named-grid-areas-no-invalid': true,
+    'no-descending-specificity': null,
+    'no-duplicate-at-import-rules': true,
+    'no-duplicate-selectors': true,
+    'no-empty-source': true,
+    'no-invalid-double-slash-comments': true,
+    'no-invalid-position-at-import-rule': [true, {ignoreAtRules: ['tailwind']}],
+    'no-irregular-whitespace': true,
+    'no-unknown-animations': null,
+    'no-unknown-custom-properties': null,
+    'number-max-precision': null,
+    'plugin/declaration-block-no-ignored-properties': true,
+    'property-allowed-list': null,
+    'property-disallowed-list': null,
+    'property-no-unknown': true,
+    'property-no-vendor-prefix': null,
+    'rule-empty-line-before': null,
+    'rule-selector-property-disallowed-list': null,
+    'scale-unlimited/declaration-strict-value': [['/color$/', 'font-weight'], {ignoreValues: '/^(inherit|transparent|unset|initial|currentcolor|none)$/', ignoreFunctions: false, disableFix: true, expandShorthand: true}],
+    'selector-anb-no-unmatchable': true,
+    'selector-attribute-name-disallowed-list': null,
+    'selector-attribute-operator-allowed-list': null,
+    'selector-attribute-operator-disallowed-list': null,
+    'selector-attribute-quotes': 'always',
+    'selector-class-pattern': null,
+    'selector-combinator-allowed-list': null,
+    'selector-combinator-disallowed-list': null,
+    'selector-disallowed-list': null,
+    'selector-id-pattern': null,
+    'selector-max-attribute': null,
+    'selector-max-class': null,
+    'selector-max-combinators': null,
+    'selector-max-compound-selectors': null,
+    'selector-max-id': null,
+    'selector-max-pseudo-class': null,
+    'selector-max-specificity': null,
+    'selector-max-type': null,
+    'selector-max-universal': null,
+    'selector-nested-pattern': null,
+    'selector-no-qualifying-type': null,
+    'selector-no-vendor-prefix': true,
+    'selector-not-notation': null,
+    'selector-pseudo-class-allowed-list': null,
+    'selector-pseudo-class-disallowed-list': null,
+    'selector-pseudo-class-no-unknown': true,
+    'selector-pseudo-element-allowed-list': null,
+    'selector-pseudo-element-colon-notation': 'double',
+    'selector-pseudo-element-disallowed-list': null,
+    'selector-pseudo-element-no-unknown': true,
+    'selector-type-case': 'lower',
+    'selector-type-no-unknown': [true, {ignore: ['custom-elements']}],
+    'shorthand-property-no-redundant-values': true,
+    'string-no-newline': true,
+    'time-min-milliseconds': null,
+    'unit-allowed-list': null,
+    'unit-disallowed-list': null,
+    'unit-no-unknown': true,
+    'value-keyword-case': null,
+    'value-no-vendor-prefix': [true, {ignoreValues: ['box', 'inline-box']}],
+  },
+};
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000000..d49e9d7a1c
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,101 @@
+import {readFileSync} from 'node:fs';
+import {env} from 'node:process';
+import {parse} from 'postcss';
+
+const isProduction = env.NODE_ENV !== 'development';
+
+function extractRootVars(css) {
+  const root = parse(css);
+  const vars = new Set();
+  root.walkRules((rule) => {
+    if (rule.selector !== ':root') return;
+    rule.each((decl) => {
+      if (decl.value && decl.prop.startsWith('--')) {
+        vars.add(decl.prop.substring(2));
+      }
+    });
+  });
+  return Array.from(vars);
+}
+
+const vars = extractRootVars([
+  readFileSync(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url), 'utf8'),
+  readFileSync(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url), 'utf8'),
+].join('\n'));
+
+export default {
+  prefix: 'tw-',
+  important: true, // the frameworks are mixed together, so tailwind needs to override other framework's styles
+  content: [
+    isProduction && '!./templates/devtest/**/*',
+    isProduction && '!./web_src/js/standalone/devtest.js',
+    '!./templates/swagger/v1_json.tmpl',
+    '!./templates/user/auth/oidc_wellknown.tmpl',
+    '!**/*_test.go',
+    '!./modules/{public,options,templates}/bindata.go',
+    './{build,models,modules,routers,services}/**/*.go',
+    './templates/**/*.tmpl',
+    './web_src/js/**/*.{js,vue}',
+  ].filter(Boolean),
+  blocklist: [
+    // classes that don't work without CSS variables from "@tailwind base" which we don't use
+    'transform', 'shadow', 'ring', 'blur', 'grayscale', 'invert', '!invert', 'filter', '!filter',
+    'backdrop-filter',
+    // we use double-class tw-hidden defined in web_src/css/helpers.css for increased specificity
+    'hidden',
+    // unneeded classes
+    '[-a-zA-Z:0-9_.]',
+  ],
+  theme: {
+    colors: {
+      // make `tw-bg-red` etc work with our CSS variables
+      ...Object.fromEntries(vars.filter((prop) => prop.startsWith('color-')).map((prop) => {
+        const color = prop.substring(6);
+        return [color, `var(--color-${color})`];
+      })),
+      inherit: 'inherit',
+      current: 'currentcolor',
+      transparent: 'transparent',
+    },
+    borderRadius: {
+      'none': '0',
+      'sm': '2px',
+      'DEFAULT': 'var(--border-radius)', // 4px
+      'md': 'var(--border-radius-medium)', // 6px
+      'lg': '8px',
+      'xl': '12px',
+      '2xl': '16px',
+      '3xl': '24px',
+      'full': 'var(--border-radius-circle)', // 50%
+    },
+    fontFamily: {
+      sans: 'var(--fonts-regular)',
+      mono: 'var(--fonts-monospace)',
+    },
+    fontWeight: {
+      light: 'var(--font-weight-light)',
+      normal: 'var(--font-weight-normal)',
+      medium: 'var(--font-weight-medium)',
+      semibold: 'var(--font-weight-semibold)',
+      bold: 'var(--font-weight-bold)',
+    },
+    fontSize: { // not using `rem` units because our root is currently 14px
+      'xs': '12px',
+      'sm': '14px',
+      'base': '16px',
+      'lg': '18px',
+      'xl': '20px',
+      '2xl': '24px',
+      '3xl': '30px',
+      '4xl': '36px',
+      '5xl': '48px',
+      '6xl': '60px',
+      '7xl': '72px',
+      '8xl': '96px',
+      '9xl': '128px',
+      ...Object.fromEntries(Array.from({length: 100}, (_, i) => {
+        return [`${i}`, `${i === 0 ? '0' : `${i}px`}`];
+      })),
+    },
+  },
+};
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 25abefae00..e140d6b5eb 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -42,7 +42,7 @@
 						<label for="port">{{ctx.Locale.Tr "admin.auths.port"}}</label>
 						<input id="port" name="port" value="{{$cfg.Port}}"  placeholder="636" required>
 					</div>
-					<div class="has-tls inline field {{if not .HasTLS}}gt-hidden{{end}}">
+					<div class="has-tls inline field {{if not .HasTLS}}tw-hidden{{end}}">
 						<div class="ui checkbox">
 							<label><strong>{{ctx.Locale.Tr "admin.auths.skip_tls_verify"}}</strong></label>
 							<input name="skip_verify" type="checkbox" {{if .Source.SkipVerify}}checked{{end}}>
@@ -113,7 +113,7 @@
 							<input type="checkbox" name="groups_enabled" class="js-ldap-group-toggle" {{if $cfg.GroupsEnabled}}checked{{end}}>
 						</div>
 					</div>
-					<div id="ldap-group-options" class="ui segment secondary {{if not $cfg.GroupsEnabled}}gt-hidden{{end}}">
+					<div id="ldap-group-options" class="ui segment secondary {{if not $cfg.GroupsEnabled}}tw-hidden{{end}}">
 						<div class="field">
 							<label>{{ctx.Locale.Tr "admin.auths.group_search_base"}}</label>
 							<input name="group_dn" value="{{$cfg.GroupDN}}" placeholder="ou=group,dc=mydomain,dc=com">
@@ -148,7 +148,7 @@
 								<input id="use_paged_search" name="use_paged_search" type="checkbox" {{if $cfg.UsePagedSearch}}checked{{end}}>
 							</div>
 						</div>
-						<div class="field required search-page-size{{if not $cfg.UsePagedSearch}} gt-hidden{{end}}">
+						<div class="field required search-page-size{{if not $cfg.UsePagedSearch}} tw-hidden{{end}}">
 							<label for="search_page_size">{{ctx.Locale.Tr "admin.auths.search_page_size"}}</label>
 							<input id="search_page_size" name="search_page_size" value="{{if $cfg.UsePagedSearch}}{{$cfg.SearchPageSize}}{{end}}">
 						</div>
@@ -205,7 +205,7 @@
 						</div>
 						<p class="help">{{ctx.Locale.Tr "admin.auths.force_smtps_helper"}}</p>
 					</div>
-					<div class="has-tls inline field {{if not .HasTLS}}gt-hidden{{end}}">
+					<div class="has-tls inline field {{if not .HasTLS}}tw-hidden{{end}}">
 						<div class="ui checkbox">
 							<label><strong>{{ctx.Locale.Tr "admin.auths.skip_tls_verify"}}</strong></label>
 							<input name="skip_verify" type="checkbox" {{if $cfg.SkipVerify}}checked{{end}}>
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl
index f32f77d5dc..f130e18f65 100644
--- a/templates/admin/auth/new.tmpl
+++ b/templates/admin/auth/new.tmpl
@@ -33,13 +33,13 @@
 				{{template "admin/auth/source/smtp" .}}
 
 				<!-- PAM -->
-				<div class="pam required field {{if not (eq .type 4)}}gt-hidden{{end}}">
+				<div class="pam required field {{if not (eq .type 4)}}tw-hidden{{end}}">
 					<label for="pam_service_name">{{ctx.Locale.Tr "admin.auths.pam_service_name"}}</label>
 					<input id="pam_service_name" name="pam_service_name" value="{{.pam_service_name}}">
 					<label for="pam_email_domain">{{ctx.Locale.Tr "admin.auths.pam_email_domain"}}</label>
 					<input id="pam_email_domain" name="pam_email_domain" value="{{.pam_email_domain}}">
 				</div>
-				<div class="pam optional field {{if not (eq .type 4)}}gt-hidden{{end}}">
+				<div class="pam optional field {{if not (eq .type 4)}}tw-hidden{{end}}">
 					<div class="ui checkbox">
 						<label for="skip_local_two_fa"><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
 						<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if .skip_local_two_fa}}checked{{end}}>
@@ -59,7 +59,7 @@
 						<input name="attributes_in_bind" type="checkbox" {{if .attributes_in_bind}}checked{{end}}>
 					</div>
 				</div>
-				<div class="ldap inline field {{if not (eq .type 2)}}gt-hidden{{end}}">
+				<div class="ldap inline field {{if not (eq .type 2)}}tw-hidden{{end}}">
 					<div class="ui checkbox">
 						<label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label>
 						<input name="is_sync_enabled" type="checkbox" {{if .is_sync_enabled}}checked{{end}}>
@@ -99,7 +99,7 @@
 				<li>GitHub</li>
 				<span>{{ctx.Locale.Tr "admin.auths.tip.github"}}</span>
 				<li>GitLab</li>
-				<span>{{ctx.Locale.Tr "admin.auths.tip.gitlab"}}</span>
+				<span>{{ctx.Locale.Tr "admin.auths.tip.gitlab_new"}}</span>
 				<li>Google</li>
 				<span>{{ctx.Locale.Tr "admin.auths.tip.google_plus"}}</span>
 				<li>OpenID Connect</li>
diff --git a/templates/admin/auth/source/ldap.tmpl b/templates/admin/auth/source/ldap.tmpl
index a584ac7628..9754aed55a 100644
--- a/templates/admin/auth/source/ldap.tmpl
+++ b/templates/admin/auth/source/ldap.tmpl
@@ -1,4 +1,4 @@
-<div class="ldap dldap field {{if not (or (eq .type 2) (eq .type 5))}}gt-hidden{{end}}">
+<div class="ldap dldap field {{if not (or (eq .type 2) (eq .type 5))}}tw-hidden{{end}}">
 	<div class="inline required field {{if .Err_SecurityProtocol}}error{{end}}">
 		<label>{{ctx.Locale.Tr "admin.auths.security_protocol"}}</label>
 		<div class="ui selection security-protocol dropdown">
@@ -20,17 +20,17 @@
 		<label for="port">{{ctx.Locale.Tr "admin.auths.port"}}</label>
 		<input id="port" name="port" value="{{.port}}"  placeholder="636">
 	</div>
-	<div class="has-tls inline field {{if not .HasTLS}}gt-hidden{{end}}">
+	<div class="has-tls inline field {{if not .HasTLS}}tw-hidden{{end}}">
 		<div class="ui checkbox">
 			<label><strong>{{ctx.Locale.Tr "admin.auths.skip_tls_verify"}}</strong></label>
 			<input name="skip_verify" type="checkbox" {{if .skip_verify}}checked{{end}}>
 		</div>
 	</div>
-	<div class="ldap field {{if not (eq .type 2)}}gt-hidden{{end}}">
+	<div class="ldap field {{if not (eq .type 2)}}tw-hidden{{end}}">
 		<label for="bind_dn">{{ctx.Locale.Tr "admin.auths.bind_dn"}}</label>
 		<input id="bind_dn" name="bind_dn" value="{{.bind_dn}}" placeholder="cn=Search,dc=mydomain,dc=com">
 	</div>
-	<div class="ldap field {{if not (eq .type 2)}}gt-hidden{{end}}">
+	<div class="ldap field {{if not (eq .type 2)}}tw-hidden{{end}}">
 		<label for="bind_password">{{ctx.Locale.Tr "admin.auths.bind_password"}}</label>
 		<input id="bind_password" name="bind_password" type="password" autocomplete="off" value="{{.bind_password}}">
 	</div>
@@ -38,7 +38,7 @@
 		<label for="user_base">{{ctx.Locale.Tr "admin.auths.user_base"}}</label>
 		<input id="user_base" name="user_base" value="{{.user_base}}" placeholder="ou=Users,dc=mydomain,dc=com">
 	</div>
-	<div class="dldap required field {{if not (eq .type 5)}}gt-hidden{{end}}">
+	<div class="dldap required field {{if not (eq .type 5)}}tw-hidden{{end}}">
 		<label for="user_dn">{{ctx.Locale.Tr "admin.auths.user_dn"}}</label>
 		<input id="user_dn" name="user_dn" value="{{.user_dn}}" placeholder="uid=%s,ou=Users,dc=mydomain,dc=com">
 	</div>
@@ -115,13 +115,13 @@
 	</div>
 	<!-- ldap group end -->
 
-	<div class="ldap inline field {{if not (eq .type 2)}}gt-hidden{{end}}">
+	<div class="ldap inline field {{if not (eq .type 2)}}tw-hidden{{end}}">
 		<div class="ui checkbox">
 			<label for="use_paged_search"><strong>{{ctx.Locale.Tr "admin.auths.use_paged_search"}}</strong></label>
 			<input id="use_paged_search" name="use_paged_search" class="use-paged-search" type="checkbox" {{if .use_paged_search}}checked{{end}}>
 		</div>
 	</div>
-	<div class="ldap field search-page-size required {{if or (not (eq .type 2)) (not .use_paged_search)}}gt-hidden{{end}}">
+	<div class="ldap field search-page-size required {{if or (not (eq .type 2)) (not .use_paged_search)}}tw-hidden{{end}}">
 		<label for="search_page_size">{{ctx.Locale.Tr "admin.auths.search_page_size"}}</label>
 		<input id="search_page_size" name="search_page_size" value="{{.search_page_size}}">
 	</div>
diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl
index 63ad77e67b..f02c5bdf30 100644
--- a/templates/admin/auth/source/oauth.tmpl
+++ b/templates/admin/auth/source/oauth.tmpl
@@ -1,4 +1,4 @@
-<div class="oauth2 field {{if not (eq .type 6)}}gt-hidden{{end}}">
+<div class="oauth2 field {{if not (eq .type 6)}}tw-hidden{{end}}">
 	<div class="inline required field">
 		<label>{{ctx.Locale.Tr "admin.auths.oauth2_provider"}}</label>
 		<div class="ui selection type dropdown">
diff --git a/templates/admin/auth/source/smtp.tmpl b/templates/admin/auth/source/smtp.tmpl
index c4b0b0e7e4..31195acf65 100644
--- a/templates/admin/auth/source/smtp.tmpl
+++ b/templates/admin/auth/source/smtp.tmpl
@@ -1,4 +1,4 @@
-<div class="smtp field {{if not (eq .type 3)}}gt-hidden{{end}}">
+<div class="smtp field {{if not (eq .type 3)}}tw-hidden{{end}}">
 	<div class="inline required field">
 		<label>{{ctx.Locale.Tr "admin.auths.smtp_auth"}}</label>
 		<div class="ui selection type dropdown">
diff --git a/templates/admin/auth/source/sspi.tmpl b/templates/admin/auth/source/sspi.tmpl
index f835e89bdf..6a3f00f9a8 100644
--- a/templates/admin/auth/source/sspi.tmpl
+++ b/templates/admin/auth/source/sspi.tmpl
@@ -1,4 +1,4 @@
-<div class="sspi field {{if not (eq .type 7)}}gt-hidden{{end}}">
+<div class="sspi field {{if not (eq .type 7)}}tw-hidden{{end}}">
 	<div class="field">
 		<div class="ui checkbox">
 			<label for="sspi_auto_create_users"><strong>{{ctx.Locale.Tr "admin.auths.sspi_auto_create_users"}}</strong></label>
diff --git a/templates/admin/base/search.tmpl b/templates/admin/base/search.tmpl
deleted file mode 100644
index 0fecb61d9e..0000000000
--- a/templates/admin/base/search.tmpl
+++ /dev/null
@@ -1,23 +0,0 @@
-<div class="ui secondary filter menu gt-ac gt-mx-0">
-	<form class="ui form ignore-dirty gt-f1">
-		<div class="ui fluid action input">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
-			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-		</div>
-	</form>
-	<!-- Sort -->
-	<div class="ui dropdown type jump item gt-mr-0">
-		<span class="text">
-			{{ctx.Locale.Tr "repo.issues.filter_sort"}}
-		</span>
-		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-		<div class="menu">
-			<a class="{{if or (eq .SortType "oldest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-			<a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-		</div>
-	</div>
-</div>
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index 1cc4b7bb09..8c16429920 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -229,7 +229,7 @@
 					<dt>{{ctx.Locale.Tr "admin.config.mailer_user"}}</dt>
 					<dd>{{if .Mailer.User}}{{.Mailer.User}}{{else}}(empty){{end}}</dd>
 					<div class="divider"></div>
-					<dt class="gt-py-2">{{ctx.Locale.Tr "admin.config.send_test_mail"}}</dt>
+					<dt class="tw-py-1">{{ctx.Locale.Tr "admin.config.send_test_mail"}}</dt>
 					<dd>
 						<form class="ui form ignore-dirty" action="{{AppSubUrl}}/admin/config/test_mail" method="post">
 							{{.CsrfTokenHtml}}
@@ -283,27 +283,6 @@
 			</dl>
 		</div>
 
-		<h4 class="ui top attached header">
-			{{ctx.Locale.Tr "admin.config.picture_config"}}
-		</h4>
-		<div class="ui attached table segment">
-			<dl class="admin-dl-horizontal">
-				<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt>
-				<dd>
-					<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}">
-						<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}>
-					</div>
-				</dd>
-				<div class="divider"></div>
-				<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt>
-				<dd>
-					<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}">
-						<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}>
-					</div>
-				</dd>
-			</dl>
-		</div>
-
 		<h4 class="ui top attached header">
 			{{ctx.Locale.Tr "admin.config.git_config"}}
 		</h4>
@@ -353,7 +332,7 @@
 				{{range $loggerName, $loggerDetail := .Loggers}}
 					<dt>{{ctx.Locale.Tr "admin.config.logger_name_fmt" $loggerName}}</dt>
 					{{if $loggerDetail.IsEnabled}}
-						<dd><pre class="gt-m-0">{{$loggerDetail.EventWriters | JsonUtils.EncodeToString | JsonUtils.PrettyIndent}}</pre></dd>
+						<dd><pre class="tw-m-0">{{$loggerDetail.EventWriters | JsonUtils.EncodeToString | JsonUtils.PrettyIndent}}</pre></dd>
 					{{else}}
 						<dd>{{ctx.Locale.Tr "admin.config.disabled_logger"}}</dd>
 					{{end}}
diff --git a/templates/admin/config_settings.tmpl b/templates/admin/config_settings.tmpl
new file mode 100644
index 0000000000..02ab5fd0fb
--- /dev/null
+++ b/templates/admin/config_settings.tmpl
@@ -0,0 +1,42 @@
+{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}}
+<h4 class="ui top attached header">
+	{{ctx.Locale.Tr "admin.config.picture_config"}}
+</h4>
+<div class="ui attached table segment">
+	<dl class="admin-dl-horizontal">
+		<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt>
+		<dd>
+			<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}">
+				<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}><label></label>
+			</div>
+		</dd>
+		<div class="divider"></div>
+		<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt>
+		<dd>
+			<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}">
+				<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}><label></label>
+			</div>
+		</dd>
+	</dl>
+</div>
+
+<h4 class="ui top attached header">
+	{{ctx.Locale.Tr "repository"}}
+</h4>
+<div class="ui attached segment">
+	<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/admin/config?key={{.SystemConfig.Repository.OpenWithEditorApps.DynKey}}">
+		<div class="field">
+			<details>
+				<summary>{{ctx.Locale.Tr "admin.config.open_with_editor_app_help"}}</summary>
+				<pre class="tw-px-4">{{.DefaultOpenWithEditorAppsString}}</pre>
+			</details>
+		</div>
+		<div class="field">
+			<textarea name="value">{{(.SystemConfig.Repository.OpenWithEditorApps.Value ctx).ToTextareaString}}</textarea>
+		</div>
+		<div class="field">
+			<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
+		</div>
+	</form>
+</div>
+{{template "admin/layout_footer" .}}
diff --git a/templates/admin/cron.tmpl b/templates/admin/cron.tmpl
index af30cc06e1..3cb641488c 100644
--- a/templates/admin/cron.tmpl
+++ b/templates/admin/cron.tmpl
@@ -5,7 +5,7 @@
 	</h4>
 	<div class="ui attached table segment">
 		<form method="post" action="{{AppSubUrl}}/admin">
-			<table class="ui very basic striped table unstackable gt-mb-0">
+			<table class="ui very basic striped table unstackable tw-mb-0">
 				<thead>
 					<tr>
 						<th></th>
diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl
index f43b4c5385..589fc5048a 100644
--- a/templates/admin/dashboard.tmpl
+++ b/templates/admin/dashboard.tmpl
@@ -2,16 +2,16 @@
 	<div class="admin-setting-content">
 		{{if .NeedUpdate}}
 			<div class="ui negative message flash-error">
-				<p>{{(ctx.Locale.Tr "admin.dashboard.new_version_hint" .RemoteVersion AppVer) | Str2html}}</p>
+				<p>{{ctx.Locale.Tr "admin.dashboard.new_version_hint" .RemoteVersion AppVer}}</p>
 			</div>
 		{{end}}
 		<h4 class="ui top attached header">
-			{{ctx.Locale.Tr "admin.dashboard.operations"}}
+			{{ctx.Locale.Tr "admin.dashboard.maintenance_operations"}}
 		</h4>
 		<div class="ui attached table segment">
 			<form method="post" action="{{AppSubUrl}}/admin">
 				{{.CsrfTokenHtml}}
-				<table class="ui very basic table gt-mt-0 gt-px-4">
+				<table class="ui very basic table tw-mt-0 tw-px-4">
 					<tbody>
 						<tr>
 							<td>{{ctx.Locale.Tr "admin.dashboard.delete_inactive_accounts"}}</td>
@@ -75,69 +75,9 @@
 		<h4 class="ui top attached header">
 			{{ctx.Locale.Tr "admin.dashboard.system_status"}}
 		</h4>
-		<div class="ui attached table segment">
-			<dl class="admin-dl-horizontal">
-				<dt>{{ctx.Locale.Tr "admin.dashboard.server_uptime"}}</dt>
-				<dd><relative-time format="duration" datetime="{{.SysStatus.StartTime}}">{{.SysStatus.StartTime}}</relative-time></dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.current_goroutine"}}</dt>
-				<dd>{{.SysStatus.NumGoroutine}}</dd>
-				<div class="divider"></div>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}</dt>
-				<dd>{{.SysStatus.MemAllocated}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}</dt>
-				<dd>{{.SysStatus.MemTotal}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}</dt>
-				<dd>{{.SysStatus.MemSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}</dt>
-				<dd>{{.SysStatus.Lookups}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}</dt>
-				<dd>{{.SysStatus.MemMallocs}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.memory_free_times"}}</dt>
-				<dd>{{.SysStatus.MemFrees}}</dd>
-				<div class="divider"></div>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}</dt>
-				<dd>{{.SysStatus.HeapAlloc}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}</dt>
-				<dd>{{.SysStatus.HeapSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}</dt>
-				<dd>{{.SysStatus.HeapIdle}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}</dt>
-				<dd>{{.SysStatus.HeapInuse}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}</dt>
-				<dd>{{.SysStatus.HeapReleased}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}</dt>
-				<dd>{{.SysStatus.HeapObjects}}</dd>
-				<div class="divider"></div>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}</dt>
-				<dd>{{.SysStatus.StackInuse}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}</dt>
-				<dd>{{.SysStatus.StackSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}</dt>
-				<dd>{{.SysStatus.MSpanInuse}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}</dt>
-				<dd>{{.SysStatus.MSpanSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}</dt>
-				<dd>{{.SysStatus.MCacheInuse}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}</dt>
-				<dd>{{.SysStatus.MCacheSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}</dt>
-				<dd>{{.SysStatus.BuckHashSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}</dt>
-				<dd>{{.SysStatus.GCSys}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}</dt>
-				<dd>{{.SysStatus.OtherSys}}</dd>
-				<div class="divider"></div>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}</dt>
-				<dd>{{.SysStatus.NextGC}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}</dt>
-				<dd>{{.SysStatus.LastGC}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}</dt>
-				<dd>{{.SysStatus.PauseTotalNs}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_pause"}}</dt>
-				<dd>{{.SysStatus.PauseNs}}</dd>
-				<dt>{{ctx.Locale.Tr "admin.dashboard.gc_times"}}</dt>
-				<dd>{{.SysStatus.NumGC}}</dd>
-			</dl>
+		{{/* TODO: make these stats work in multi-server deployments, likely needs per-server stats in DB */}}
+		<div hx-get="{{$.Link}}/system_status" hx-swap="morph:innerHTML" hx-trigger="every 5s" hx-indicator=".divider" class="ui attached table segment">
+			{{template "admin/system_status" .}}
 		</div>
 	</div>
 {{template "admin/layout_footer" .}}
diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl
index bcd80368e6..388863df9b 100644
--- a/templates/admin/emails/list.tmpl
+++ b/templates/admin/emails/list.tmpl
@@ -4,24 +4,21 @@
 			{{ctx.Locale.Tr "admin.emails.email_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
 		</h4>
 		<div class="ui attached segment">
-			<div class="ui secondary filter menu gt-ac gt-mx-0">
-				<form class="ui form ignore-dirty gt-f1">
-					<div class="ui fluid action input">
-						{{template "shared/searchinput" dict "Value" .Keyword}}
-						<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-					</div>
+			<div class="ui secondary filter menu tw-items-center tw-mx-0">
+				<form class="ui form ignore-dirty tw-flex-1">
+					{{template "shared/search/combo" dict "Value" .Keyword}}
 				</form>
 				<!-- Sort -->
-				<div class="ui dropdown type jump item gt-mr-0">
+				<div class="ui dropdown type jump item tw-mr-0">
 					<span class="text">
 						{{ctx.Locale.Tr "repo.issues.filter_sort"}}
 					</span>
 					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 					<div class="menu">
-						<a class="{{if or (eq .SortType "email") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=email&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.email"}}</a>
-						<a class="{{if eq .SortType "reverseemail"}}active {{end}}item" href="{{$.Link}}?sort=reverseemail&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.email_reverse"}}</a>
-						<a class="{{if eq .SortType "username"}}active {{end}}item" href="{{$.Link}}?sort=username&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.name"}}</a>
-						<a class="{{if eq .SortType "reverseusername"}}active {{end}}item" href="{{$.Link}}?sort=reverseusername&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.name_reverse"}}</a>
+						<a class="{{if or (eq .SortType "email") (not .SortType)}}active {{end}}item" href="?sort=email&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.email"}}</a>
+						<a class="{{if eq .SortType "reverseemail"}}active {{end}}item" href="?sort=reverseemail&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.email_reverse"}}</a>
+						<a class="{{if eq .SortType "username"}}active {{end}}item" href="?sort=username&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.name"}}</a>
+						<a class="{{if eq .SortType "reverseusername"}}active {{end}}item" href="?sort=reverseusername&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.name_reverse"}}</a>
 					</div>
 				</div>
 			</div>
@@ -47,8 +44,8 @@
 					{{range .Emails}}
 						<tr>
 							<td><a href="{{AppSubUrl}}/{{.Name | PathEscape}}">{{.Name}}</a></td>
-							<td class="gt-ellipsis gt-max-width-12rem">{{.FullName}}</td>
-							<td class="gt-ellipsis gt-max-width-12rem">{{.Email}}</td>
+							<td class="gt-ellipsis tw-max-w-48">{{.FullName}}</td>
+							<td class="gt-ellipsis tw-max-w-48">{{.Email}}</td>
 							<td>{{if .IsPrimary}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 							<td>
 								{{if .CanChange}}
diff --git a/templates/admin/layout_head.tmpl b/templates/admin/layout_head.tmpl
index 0067f336e0..c1f5fb3314 100644
--- a/templates/admin/layout_head.tmpl
+++ b/templates/admin/layout_head.tmpl
@@ -1,9 +1,9 @@
 {{template "base/head" .ctxData}}
 <div role="main" aria-label="{{.ctxData.Title}}" class="page-content {{.pageClass}}">
-	<div class="ui container gt-mb-4">
+	<div class="ui container">
 		{{template "base/alert" .ctxData}}
 	</div>
-	<div class="ui container flex-container">
+	<div class="ui container fluid padded flex-container">
 		{{template "admin/navbar" .ctxData}}
 		<div class="flex-container-main">
 			{{/* block: admin-setting-content */}}
diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl
index fa79f0f759..1b3b9d6efc 100644
--- a/templates/admin/navbar.tmpl
+++ b/templates/admin/navbar.tmpl
@@ -1,12 +1,18 @@
 <div class="flex-container-nav">
 	<div class="ui fluid vertical menu">
 		<div class="header item">{{ctx.Locale.Tr "admin.settings"}}</div>
-		<a class="{{if .PageIsAdminDashboard}}active {{end}}item" href="{{AppSubUrl}}/admin">
-			{{ctx.Locale.Tr "admin.dashboard"}}
-		</a>
-		<a class="{{if .PageIsAdminSelfCheck}}active {{end}}item" href="{{AppSubUrl}}/admin/self_check">
-			{{ctx.Locale.Tr "admin.self_check"}}
-		</a>
+
+		<details class="item toggleable-item" {{if or .PageIsAdminDashboard .PageIsAdminSelfCheck}}open{{end}}>
+			<summary>{{ctx.Locale.Tr "admin.maintenance"}}</summary>
+			<div class="menu">
+				<a class="{{if .PageIsAdminDashboard}}active {{end}}item" href="{{AppSubUrl}}/admin">
+					{{ctx.Locale.Tr "admin.dashboard"}}
+				</a>
+				<a class="{{if .PageIsAdminSelfCheck}}active {{end}}item" href="{{AppSubUrl}}/admin/self_check">
+					{{ctx.Locale.Tr "admin.self_check"}}
+				</a>
+			</div>
+		</details>
 		<details class="item toggleable-item" {{if or .PageIsAdminUsers .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}>
 			<summary>{{ctx.Locale.Tr "admin.identity_access"}}</summary>
 			<div class="menu">
@@ -75,9 +81,17 @@
 			</div>
 		</details>
 		{{end}}
-		<a class="{{if .PageIsAdminConfig}}active {{end}}item" href="{{AppSubUrl}}/admin/config">
-			{{ctx.Locale.Tr "admin.config"}}
-		</a>
+		<details class="item toggleable-item" {{if or .PageIsAdminConfig}}open{{end}}>
+			<summary>{{ctx.Locale.Tr "admin.config"}}</summary>
+			<div class="menu">
+				<a class="{{if .PageIsAdminConfigSummary}}active {{end}}item" href="{{AppSubUrl}}/admin/config">
+					{{ctx.Locale.Tr "admin.config_summary"}}
+				</a>
+				<a class="{{if .PageIsAdminConfigSettings}}active {{end}}item" href="{{AppSubUrl}}/admin/config/settings">
+					{{ctx.Locale.Tr "admin.config_settings"}}
+				</a>
+			</div>
+		</details>
 		<a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/admin/notices">
 			{{ctx.Locale.Tr "admin.notices"}}
 		</a>
diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl
index ed410425b5..5ea003e5ec 100644
--- a/templates/admin/notice.tmpl
+++ b/templates/admin/notice.tmpl
@@ -17,10 +17,10 @@
 			<tbody>
 				{{range .Notices}}
 					<tr>
-						<td><div class="ui checkbox gt-df" data-id="{{.ID}}"><input type="checkbox"></div></td>
+						<td><div class="ui checkbox tw-flex" data-id="{{.ID}}"><input type="checkbox"></div></td>
 						<td>{{.ID}}</td>
 						<td>{{ctx.Locale.Tr .TrStr}}</td>
-						<td class="view-detail auto-ellipsis" style="width: 80%;"><span class="notice-description">{{.Description}}</span></td>
+						<td class="view-detail auto-ellipsis tw-w-4/5"><span class="notice-description">{{.Description}}</span></td>
 						<td nowrap>{{DateTime "short" .CreatedUnix}}</td>
 						<td class="view-detail"><a href="#">{{svg "octicon-note" 16}}</a></td>
 					</tr>
@@ -31,7 +31,7 @@
 						<tr>
 							<th></th>
 							<th colspan="5">
-								<form class="gt-float-right" method="post" action="{{AppSubUrl}}/admin/notices/empty">
+								<form class="tw-float-right" method="post" action="{{AppSubUrl}}/admin/notices/empty">
 									{{.CsrfTokenHtml}}
 									<button type="submit" class="ui red small button">{{ctx.Locale.Tr "admin.notices.delete_all"}}</button>
 								</form>
@@ -49,8 +49,8 @@
 										</div>
 									</div>
 								</div>
-								<button class="ui small teal button" id="delete-selection" data-link="{{.Link}}/delete" data-redirect="{{.Link}}?page={{.Page.Paginater.Current}}">
-									{{ctx.Locale.Tr "admin.notices.delete_selected"}}
+								<button class="ui small teal button" id="delete-selection" data-link="{{.Link}}/delete" data-redirect="?page={{.Page.Paginater.Current}}">
+									<span class="text">{{ctx.Locale.Tr "admin.notices.delete_selected"}}</span>
 								</button>
 							</th>
 						</tr>
diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl
index 0d79456b47..987ceab1e0 100644
--- a/templates/admin/org/list.tmpl
+++ b/templates/admin/org/list.tmpl
@@ -7,7 +7,26 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
-			{{template "admin/base/search" .}}
+			<div class="ui secondary filter menu tw-items-center tw-mx-0">
+				<form class="ui form ignore-dirty tw-flex-1">
+					{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.org_kind")}}
+				</form>
+				<!-- Sort -->
+				<div class="ui dropdown type jump item tw-mr-0">
+					<span class="text">
+						{{ctx.Locale.Tr "repo.issues.filter_sort"}}
+					</span>
+					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+					<div class="menu">
+						<a class="{{if or (eq .SortType "oldest") (not .SortType)}}active {{end}}item" href="?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+						<a class="{{if eq .SortType "newest"}}active {{end}}item" href="?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
+						<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
+						<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
+						<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+						<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+					</div>
+				</div>
+			</div>
 		</div>
 		<div class="ui attached table segment">
 			<table class="ui very basic striped table unstackable">
diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl
index 5cfd9ddefa..863f11da25 100644
--- a/templates/admin/packages/list.tmpl
+++ b/templates/admin/packages/list.tmpl
@@ -13,16 +13,16 @@
 		</h4>
 		<div class="ui attached segment">
 			<form class="ui form ignore-dirty">
-				<div class="ui fluid action input">
-					{{template "shared/searchinput" dict "Value" .Query}}
-					<select class="ui dropdown" name="type">
+				<div class="ui small fluid action input">
+					{{template "shared/search/input" dict "Value" .Query}}
+					<select class="ui small dropdown" name="type">
 						<option value="">{{ctx.Locale.Tr "packages.filter.type"}}</option>
 						<option value="all">{{ctx.Locale.Tr "packages.filter.type.all"}}</option>
 						{{range $type := .AvailableTypes}}
 						<option{{if eq $.PackageType $type}} selected="selected"{{end}} value="{{$type}}">{{$type.Name}}</option>
 						{{end}}
 					</select>
-					<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
+					{{template "shared/search/button"}}
 				</div>
 			</form>
 		</div>
@@ -62,8 +62,8 @@
 								{{end}}
 							</td>
 							<td>{{.Package.Type.Name}}</td>
-							<td class="gt-ellipsis gt-max-width-12rem">{{.Package.Name}}</td>
-							<td class="gt-ellipsis gt-max-width-12rem"><a href="{{.FullWebLink}}">{{.Version.Version}}</a></td>
+							<td class="gt-ellipsis tw-max-w-48">{{.Package.Name}}</td>
+							<td class="gt-ellipsis tw-max-w-48"><a href="{{.VersionWebLink}}">{{.Version.Version}}</a></td>
 							<td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td>
 							<td>
 							{{if .Repository}}
@@ -88,7 +88,7 @@
 		{{ctx.Locale.Tr "packages.settings.delete"}}
 	</div>
 	<div class="content">
-		{{ctx.Locale.Tr "packages.settings.delete.notice" `<span class="name"></span>` `<span class="dataVersion"></span>` | Safe}}
+		{{ctx.Locale.Tr "packages.settings.delete.notice" (`<span class="name"></span>`|SafeHTML) (`<span class="dataVersion"></span>`|SafeHTML)}}
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/admin/queue_manage.tmpl b/templates/admin/queue_manage.tmpl
index 80214d1021..dc0196fc6a 100644
--- a/templates/admin/queue_manage.tmpl
+++ b/templates/admin/queue_manage.tmpl
@@ -30,7 +30,7 @@
 								-
 							{{else}}
 								{{$sum}}
-								<form action="{{$.Link}}/remove-all-items" method="post" class="gt-dib gt-ml-4">
+								<form action="{{$.Link}}/remove-all-items" method="post" class="tw-inline-block tw-ml-4">
 									{{$.CsrfTokenHtml}}
 									<button class="ui tiny basic red button">{{ctx.Locale.Tr "admin.monitor.queue.settings.remove_all_items"}}</button>
 								</form>
diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl
index fdba0734a2..4b27d87a45 100644
--- a/templates/admin/repo/list.tmpl
+++ b/templates/admin/repo/list.tmpl
@@ -7,7 +7,7 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
-			{{template "admin/repo/search" .}}
+			{{template "shared/repo_search" .}}
 		</div>
 		<div class="ui attached table segment">
 			<table class="ui very basic striped table unstackable">
@@ -19,13 +19,13 @@
 							{{ctx.Locale.Tr "admin.repos.name"}}
 							{{SortArrow "alphabetically" "reversealphabetically" $.SortType false}}
 						</th>
-						<th>{{ctx.Locale.Tr "admin.repos.watches"}}</th>
+						<th>{{ctx.Locale.Tr "repo.watchers"}}</th>
 						<th  data-sortt-asc="moststars" data-sortt-desc="feweststars">
-							{{ctx.Locale.Tr "admin.repos.stars"}}
+							{{ctx.Locale.Tr "repo.stars"}}
 							{{SortArrow "moststars" "feweststars" $.SortType false}}
 						</th>
 						<th  data-sortt-asc="mostforks" data-sortt-desc="fewestforks">
-							{{ctx.Locale.Tr "admin.repos.forks"}}
+							{{ctx.Locale.Tr "repo.forks"}}
 							{{SortArrow "mostforks" "fewestforks" $.SortType false}}
 						</th>
 						<th>{{ctx.Locale.Tr "admin.repos.issues"}}</th>
@@ -101,7 +101,7 @@
 	</div>
 	<div class="content">
 		<p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p>
-		{{ctx.Locale.Tr "repo.settings.delete_notices_2" `<span class="name"></span>` | Safe}}<br>
+		{{ctx.Locale.Tr "repo.settings.delete_notices_2" (`<span class="name"></span>`|SafeHTML)}}<br>
 		{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}<br>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
diff --git a/templates/admin/repo/search.tmpl b/templates/admin/repo/search.tmpl
deleted file mode 100644
index 247ec5491a..0000000000
--- a/templates/admin/repo/search.tmpl
+++ /dev/null
@@ -1,29 +0,0 @@
-<div class="ui secondary filter menu gt-ac gt-mx-0">
-	<form class="ui form ignore-dirty gt-f1">
-		<div class="ui fluid action input">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
-			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-		</div>
-	</form>
-	<!-- Sort -->
-	<div class="ui dropdown type jump item gt-mr-0">
-		<span class="text">
-			{{ctx.Locale.Tr "repo.issues.filter_sort"}}
-		</span>
-		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-		<div class="menu">
-			<a class="{{if or (eq .SortType "oldest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-			<a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-			<a class="{{if eq .SortType "moststars"}}active {{end}}item" href="{{$.Link}}?sort=moststars&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</a>
-			<a class="{{if eq .SortType "feweststars"}}active {{end}}item" href="{{$.Link}}?sort=feweststars&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</a>
-			<a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?sort=mostforks&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
-			<a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?sort=fewestforks&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
-			<a class="{{if eq .SortType "size"}}active {{end}}item" href="{{$.Link}}?sort=size&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.by_size"}}</a>
-			<a class="{{if eq .SortType "reversesize"}}active {{end}}item" href="{{$.Link}}?sort=reversesize&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_by_size"}}</a>
-		</div>
-	</div>
-</div>
diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl
index fb4f16791d..6a8e203694 100644
--- a/templates/admin/repo/unadopted.tmpl
+++ b/templates/admin/repo/unadopted.tmpl
@@ -8,10 +8,10 @@
 		</h4>
 		<div class="ui attached segment">
 			<form class="ui form ignore-dirty">
-				<div class="ui fluid action input">
-				<input name="search" value="true" type="hidden">
-				<input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "repo.adopt_search"}}" autofocus>
-				<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
+				<div class="ui small fluid action input">
+					<input name="search" value="true" type="hidden">
+					<input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "repo.adopt_search"}}" autofocus>
+					{{template "shared/search/button"}}
 				</div>
 			</form>
 		</div>
@@ -20,10 +20,10 @@
 				{{if .Dirs}}
 					<div class="ui aligned divided list">
 						{{range $dirI, $dir := .Dirs}}
-							<div class="item gt-df gt-ac">
-								<span class="gt-f1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span>
+							<div class="item tw-flex tw-items-center">
+								<span class="tw-flex-1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span>
 								<div>
-									<button class="ui button primary show-modal gt-p-3" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button>
+									<button class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button>
 									<div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}">
 										<div class="header">
 											<span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span>
@@ -40,7 +40,7 @@
 											{{template "base/modal_actions_confirm"}}
 										</form>
 									</div>
-									<button class="ui button red show-modal gt-p-3" data-modal="#delete-unadopted-modal-{{$dirI}}">{{svg "octicon-x"}} {{ctx.Locale.Tr "repo.delete_preexisting_label"}}</button>
+									<button class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}">{{svg "octicon-x"}} {{ctx.Locale.Tr "repo.delete_preexisting_label"}}</button>
 									<div class="ui g-modal-confirm modal" id="delete-unadopted-modal-{{$dirI}}">
 										<div class="header">
 											<span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span>
diff --git a/templates/admin/self_check.tmpl b/templates/admin/self_check.tmpl
index 6bca01ec65..a6c2ac1ac9 100644
--- a/templates/admin/self_check.tmpl
+++ b/templates/admin/self_check.tmpl
@@ -4,33 +4,47 @@
 	<h4 class="ui top attached header">
 		{{ctx.Locale.Tr "admin.self_check"}}
 	</h4>
+
+	{{if .StartupProblems}}
 	<div class="ui attached segment">
-		{{if .DatabaseCheckHasProblems}}
-			{{if .DatabaseType.IsMySQL}}
-				<div class="gt-p-3">{{ctx.Locale.Tr "admin.self_check.database_fix_mysql"}}</div>
-			{{else if .DatabaseType.IsMSSQL}}
-				<div class="gt-p-3">{{ctx.Locale.Tr "admin.self_check.database_fix_mssql"}}</div>
-			{{end}}
-			{{if .DatabaseCheckCollationMismatch}}
-				<div class="ui red message">{{ctx.Locale.Tr "admin.self_check.database_collation_mismatch" .DatabaseCheckResult.ExpectedCollation}}</div>
-			{{end}}
-			{{if .DatabaseCheckCollationCaseInsensitive}}
-				<div class="ui warning message">{{ctx.Locale.Tr "admin.self_check.database_collation_case_insensitive" .DatabaseCheckResult.DatabaseCollation}}</div>
-			{{end}}
-			{{if .DatabaseCheckInconsistentCollationColumns}}
-				<div class="ui red message">
-					{{ctx.Locale.Tr "admin.self_check.database_inconsistent_collation_columns" .DatabaseCheckResult.DatabaseCollation}}
-					<ul class="gt-w-full">
-					{{range .DatabaseCheckInconsistentCollationColumns}}
-						<li>{{.}}</li>
-					{{end}}
-					</ul>
-				</div>
-			{{end}}
-		{{else}}
-			<div class="gt-p-3">{{ctx.Locale.Tr "admin.self_check.no_problem_found"}}</div>
+		<div class="ui warning message">
+			<div>{{ctx.Locale.Tr "admin.self_check.startup_warnings"}}</div>
+			<ul class="tw-w-full">{{range .StartupProblems}}<li>{{.}}</li>{{end}}</ul>
+		</div>
+	</div>
+	{{end}}
+
+	{{if .DatabaseCheckHasProblems}}
+	<div class="ui attached segment">
+		{{if .DatabaseType.IsMySQL}}
+			<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mysql"}}</div>
+		{{else if .DatabaseType.IsMSSQL}}
+			<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mssql"}}</div>
+		{{end}}
+		{{if .DatabaseCheckCollationMismatch}}
+			<div class="ui red message">{{ctx.Locale.Tr "admin.self_check.database_collation_mismatch" .DatabaseCheckResult.ExpectedCollation}}</div>
+		{{end}}
+		{{if .DatabaseCheckCollationCaseInsensitive}}
+			<div class="ui warning message">{{ctx.Locale.Tr "admin.self_check.database_collation_case_insensitive" .DatabaseCheckResult.DatabaseCollation}}</div>
+		{{end}}
+		{{if .DatabaseCheckInconsistentCollationColumns}}
+			<div class="ui red message">
+				{{ctx.Locale.Tr "admin.self_check.database_inconsistent_collation_columns" .DatabaseCheckResult.DatabaseCollation}}
+				<ul class="tw-w-full">
+				{{range .DatabaseCheckInconsistentCollationColumns}}
+					<li>{{.}}</li>
+				{{end}}
+				</ul>
+			</div>
 		{{end}}
 	</div>
+	{{end}}
+
+	{{if and (not .StartupProblems) (not .DatabaseCheckHasProblems)}}
+	<div class="ui attached segment">
+		{{ctx.Locale.Tr "admin.self_check.no_problem_found"}}
+	</div>
+	{{end}}
 </div>
 
 {{template "admin/layout_footer" .}}
diff --git a/templates/admin/stacktrace-row.tmpl b/templates/admin/stacktrace-row.tmpl
index ffb8bf812f..694bf56d96 100644
--- a/templates/admin/stacktrace-row.tmpl
+++ b/templates/admin/stacktrace-row.tmpl
@@ -1,6 +1,6 @@
 <div class="item">
-	<div class="gt-df gt-ac">
-		<div class="icon gt-ml-3 gt-mr-3">
+	<div class="tw-flex tw-items-center">
+		<div class="icon tw-ml-2 tw-mr-2">
 			{{if eq .Process.Type "request"}}
 				{{svg "octicon-globe" 16}}
 			{{else if eq .Process.Type "system"}}
@@ -11,7 +11,7 @@
 				{{svg "octicon-code" 16}}
 			{{end}}
 		</div>
-		<div class="content gt-f1">
+		<div class="content tw-flex-1">
 			<div class="header">{{.Process.Description}}</div>
 			<div class="description">{{if ne .Process.Type "none"}}{{TimeSince .Process.Start ctx.Locale}}{{end}}</div>
 		</div>
@@ -22,14 +22,14 @@
 		</div>
 	</div>
 	{{if .Process.Stacks}}
-		<div class="divided list gt-ml-3">
+		<div class="divided list tw-ml-2">
 			{{range .Process.Stacks}}
 				<div class="item">
 					<details>
 						<summary>
 							<div class="flex-text-inline">
-								<div class="header gt-ml-3">
-									<span class="icon gt-mr-3">{{svg "octicon-code" 16}}</span>{{.Description}}{{if gt .Count 1}} * {{.Count}}{{end}}
+								<div class="header tw-ml-2">
+									<span class="icon tw-mr-2">{{svg "octicon-code" 16}}</span>{{.Description}}{{if gt .Count 1}} * {{.Count}}{{end}}
 								</div>
 								<div class="description">
 									{{range .Labels}}
@@ -40,9 +40,9 @@
 						</summary>
 						<div class="list">
 							{{range .Entry}}
-								<div class="item gt-df gt-ac">
-									<span class="icon gt-mr-4">{{svg "octicon-dot-fill" 16}}</span>
-									<div class="content gt-f1">
+								<div class="item tw-flex tw-items-center">
+									<span class="icon tw-mr-4">{{svg "octicon-dot-fill" 16}}</span>
+									<div class="content tw-flex-1">
 										<div class="header"><code>{{.Function}}</code></div>
 										<div class="description"><code>{{.File}}:{{.Line}}</code></div>
 									</div>
diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl
index 894e41f8d7..e324570c96 100644
--- a/templates/admin/stacktrace.tmpl
+++ b/templates/admin/stacktrace.tmpl
@@ -1,11 +1,11 @@
 {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}}
 <div class="admin-setting-content">
 
-	<div class="gt-df gt-ac">
-		<div class="gt-f1">
+	<div class="tw-flex tw-items-center">
+		<div class="tw-flex-1">
 			<div class="ui compact small menu">
-				<a class="{{if eq .ShowGoroutineList "process"}}active {{end}}item" href="{{.Link}}?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a>
-				<a class="{{if eq .ShowGoroutineList "stacktrace"}}active {{end}}item" href="{{.Link}}?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a>
+				<a class="{{if eq .ShowGoroutineList "process"}}active {{end}}item" href="?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a>
+				<a class="{{if eq .ShowGoroutineList "stacktrace"}}active {{end}}item" href="?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a>
 			</div>
 		</div>
 		<form target="_blank" action="{{AppSubUrl}}/admin/monitor/diagnosis" class="ui form">
@@ -39,7 +39,7 @@
 		{{ctx.Locale.Tr "admin.monitor.process.cancel"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" `<span class="name"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (`<span class="name"></span>`|SafeHTML)}}</p>
 		<p>{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
diff --git a/templates/admin/system_status.tmpl b/templates/admin/system_status.tmpl
new file mode 100644
index 0000000000..7b5c9be6cc
--- /dev/null
+++ b/templates/admin/system_status.tmpl
@@ -0,0 +1,62 @@
+<dl class="admin-dl-horizontal">
+	<dt>{{ctx.Locale.Tr "admin.dashboard.server_uptime"}}</dt>
+	<dd><relative-time format="duration" datetime="{{.SysStatus.StartTime}}">{{.SysStatus.StartTime}}</relative-time></dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.current_goroutine"}}</dt>
+	<dd>{{.SysStatus.NumGoroutine}}</dd>
+	<div class="divider"></div>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}</dt>
+	<dd>{{.SysStatus.MemAllocated}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}</dt>
+	<dd>{{.SysStatus.MemTotal}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}</dt>
+	<dd>{{.SysStatus.MemSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}</dt>
+	<dd>{{.SysStatus.Lookups}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}</dt>
+	<dd>{{.SysStatus.MemMallocs}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.memory_free_times"}}</dt>
+	<dd>{{.SysStatus.MemFrees}}</dd>
+	<div class="divider"></div>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}</dt>
+	<dd>{{.SysStatus.HeapAlloc}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}</dt>
+	<dd>{{.SysStatus.HeapSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}</dt>
+	<dd>{{.SysStatus.HeapIdle}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}</dt>
+	<dd>{{.SysStatus.HeapInuse}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}</dt>
+	<dd>{{.SysStatus.HeapReleased}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}</dt>
+	<dd>{{.SysStatus.HeapObjects}}</dd>
+	<div class="divider"></div>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}</dt>
+	<dd>{{.SysStatus.StackInuse}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}</dt>
+	<dd>{{.SysStatus.StackSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}</dt>
+	<dd>{{.SysStatus.MSpanInuse}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}</dt>
+	<dd>{{.SysStatus.MSpanSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}</dt>
+	<dd>{{.SysStatus.MCacheInuse}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}</dt>
+	<dd>{{.SysStatus.MCacheSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}</dt>
+	<dd>{{.SysStatus.BuckHashSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}</dt>
+	<dd>{{.SysStatus.GCSys}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}</dt>
+	<dd>{{.SysStatus.OtherSys}}</dd>
+	<div class="divider"></div>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}</dt>
+	<dd>{{.SysStatus.NextGC}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}</dt>
+	<dd><relative-time format="duration" datetime="{{.SysStatus.LastGCTime}}">{{.SysStatus.LastGCTime}}</relative-time></dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}</dt>
+	<dd>{{.SysStatus.PauseTotalNs}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_pause"}}</dt>
+	<dd>{{.SysStatus.PauseNs}}</dd>
+	<dt>{{ctx.Locale.Tr "admin.dashboard.gc_times"}}</dt>
+	<dd>{{.SysStatus.NumGC}}</dd>
+</dl>
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index fcb8ce0827..41b00defb4 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -53,7 +53,7 @@
 					</div>
 				</div>
 
-				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .User.LoginSource 0}}gt-hidden{{end}}">
+				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .User.LoginSource 0}}tw-hidden{{end}}">
 					<label for="login_name">{{ctx.Locale.Tr "admin.users.auth_login_name"}}</label>
 					<input id="login_name" name="login_name" value="{{.User.LoginName}}" autofocus>
 				</div>
@@ -65,11 +65,26 @@
 					<label for="email">{{ctx.Locale.Tr "email"}}</label>
 					<input id="email" name="email" type="email" value="{{.User.Email}}" autofocus required>
 				</div>
-				<div class="local field {{if .Err_Password}}error{{end}} {{if not (or (.User.IsLocal) (.User.IsOAuth2))}}gt-hidden{{end}}">
+				<div class="local field {{if .Err_Password}}error{{end}} {{if not (or (.User.IsLocal) (.User.IsOAuth2))}}tw-hidden{{end}}">
 					<label for="password">{{ctx.Locale.Tr "password"}}</label>
 					<input id="password" name="password" type="password" autocomplete="new-password">
 					<p class="help">{{ctx.Locale.Tr "admin.users.password_helper"}}</p>
 				</div>
+
+				<div class="field {{if .Err_Language}}error{{end}}">
+					<label for="language">{{ctx.Locale.Tr "settings.language"}}</label>
+					<div class="ui selection dropdown">
+						<input name="language" type="hidden" value="{{.User.Language}}">
+						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+						<div class="text">{{range .AllLangs}}{{if eq $.User.Language .Lang}}{{.Name}}{{end}}{{end}}</div>
+						<div class="menu">
+						{{range .AllLangs}}
+							<div class="item{{if eq $.User.Language .Lang}} active selected{{end}}" data-value="{{.Lang}}">{{.Name}}</div>
+						{{end}}
+						</div>
+					</div>
+				</div>
+
 				<div class="field {{if .Err_Website}}error{{end}}">
 					<label for="website">{{ctx.Locale.Tr "settings.website"}}</label>
 					<input id="website" name="website" type="url" value="{{.User.Website}}" placeholder="http://mydomain.com or https://mydomain.com" maxlength="255">
@@ -113,13 +128,13 @@
 						<input name="restricted" type="checkbox" {{if .User.IsRestricted}}checked{{end}}>
 					</div>
 				</div>
-				<div class="inline field {{if DisableGitHooks}}gt-hidden{{end}}">
+				<div class="inline field {{if DisableGitHooks}}tw-hidden{{end}}">
 					<div class="ui checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.users.allow_git_hook_tooltip"}}">
 						<label><strong>{{ctx.Locale.Tr "admin.users.allow_git_hook"}}</strong></label>
 						<input name="allow_git_hook" type="checkbox" {{if .User.CanEditGitHook}}checked{{end}} {{if DisableGitHooks}}disabled{{end}}>
 					</div>
 				</div>
-				<div class="inline field {{if or (DisableImportLocal) (.DisableMigrations)}}gt-hidden{{end}}">
+				<div class="inline field {{if or (DisableImportLocal) (.DisableMigrations)}}tw-hidden{{end}}">
 					<div class="ui checkbox">
 						<label><strong>{{ctx.Locale.Tr "admin.users.allow_import_local"}}</strong></label>
 						<input name="allow_import_local" type="checkbox" {{if .User.CanImportLocal}}checked{{end}} {{if DisableImportLocal}}disabled{{end}}>
@@ -166,7 +181,7 @@
 						<label>{{ctx.Locale.Tr "settings.lookup_avatar_by_mail"}}</label>
 					</div>
 				</div>
-				<div class="field gt-pl-4 {{if .Err_Gravatar}}error{{end}}">
+				<div class="field tw-pl-4 {{if .Err_Gravatar}}error{{end}}">
 					<label for="gravatar">Avatar {{ctx.Locale.Tr "email"}}</label>
 					<input id="gravatar" name="gravatar" value="{{.User.AvatarEmail}}">
 				</div>
@@ -179,7 +194,7 @@
 					</div>
 				</div>
 
-				<div class="inline field gt-pl-4">
+				<div class="inline field tw-pl-4">
 					<label for="avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
 					<input name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
 				</div>
diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl
index 8fdc80fc70..528d047507 100644
--- a/templates/admin/user/list.tmpl
+++ b/templates/admin/user/list.tmpl
@@ -15,7 +15,7 @@
 					<div class="ui dropdown type jump item">
 						<span class="text">{{ctx.Locale.Tr "admin.users.list_status_filter.menu_text"}}</span>
 						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-						<div class="menu">
+						<div class="menu flex-items-menu">
 							<a class="item j-reset-status-filter">{{ctx.Locale.Tr "admin.users.list_status_filter.reset"}}</a>
 							<div class="divider"></div>
 							<label class="item"><input type="radio" name="status_filter[is_admin]" value="1"> {{ctx.Locale.Tr "admin.users.list_status_filter.is_admin"}}</label>
@@ -52,11 +52,7 @@
 					</div>
 				</div>
 
-				<!-- Search Text -->
-				<div class="ui fluid action input">
-					{{template "shared/searchinput" dict "Value" .Keyword}}
-					<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-				</div>
+				{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}}
 			</form>
 		</div>
 		<div class="ui attached table segment">
@@ -96,7 +92,7 @@
 									<span class="ui mini label">{{ctx.Locale.Tr "admin.users.remote"}}</span>
 								{{end}}
 							</td>
-							<td class="gt-ellipsis gt-max-width-12rem">{{.Email}}</td>
+							<td class="gt-ellipsis tw-max-w-48">{{.Email}}</td>
 							<td>{{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 							<td>{{if .IsRestricted}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 							<td>{{if index $.UsersTwoFaStatus .ID}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
@@ -107,7 +103,7 @@
 								<td><span>{{ctx.Locale.Tr "admin.users.never_login"}}</span></td>
 							{{end}}
 							<td>
-								<div class="gt-df gt-gap-3">
+								<div class="tw-flex tw-gap-2">
 									<a href="{{$.Link}}/{{.ID}}" data-tooltip-content="{{ctx.Locale.Tr "admin.users.details"}}">{{svg "octicon-person"}}</a>
 									<a href="{{$.Link}}/{{.ID}}/edit" data-tooltip-content="{{ctx.Locale.Tr "edit"}}">{{svg "octicon-pencil"}}</a>
 								</div>
diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl
index 81f70511d0..b04ebc4b60 100644
--- a/templates/admin/user/new.tmpl
+++ b/templates/admin/user/new.tmpl
@@ -26,7 +26,7 @@
 				<div class="inline field {{if .Err_Visibility}}error{{end}}">
 					<span class="inline required field"><label for="visibility">{{ctx.Locale.Tr "settings.visibility"}}</label></span>
 					<div class="ui selection type dropdown">
-						<input type="hidden" id="visibility" name="visibility" value="{{if .visibility}}{{.visibility}}{{else}}{{printf "%d" .DefaultUserVisibilityMode}}{{end}}">
+						<input type="hidden" id="visibility" name="visibility" value="{{if .visibility}}{{printf "%d" .visibility}}{{else}}{{printf "%d" .DefaultUserVisibilityMode}}{{end}}">
 						<div class="text">
 							{{if .DefaultUserVisibilityMode.IsPublic}}{{ctx.Locale.Tr "settings.visibility.public"}}{{end}}
 							{{if .DefaultUserVisibilityMode.IsLimited}}{{ctx.Locale.Tr "settings.visibility.limited"}}{{end}}
@@ -47,7 +47,7 @@
 					</div>
 				</div>
 
-				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .login_type "0-0"}}gt-hidden{{end}}">
+				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .login_type "0-0"}}tw-hidden{{end}}">
 					<label for="login_name">{{ctx.Locale.Tr "admin.users.auth_login_name"}}</label>
 					<input id="login_name" name="login_name" value="{{.login_name}}">
 				</div>
@@ -59,12 +59,12 @@
 					<label for="email">{{ctx.Locale.Tr "email"}}</label>
 					<input id="email" name="email" type="email" value="{{.email}}" required>
 				</div>
-				<div class="required local field {{if .Err_Password}}error{{end}} {{if not (eq .login_type "0-0")}}gt-hidden{{end}}">
+				<div class="required local field {{if .Err_Password}}error{{end}} {{if not (eq .login_type "0-0")}}tw-hidden{{end}}">
 					<label for="password">{{ctx.Locale.Tr "password"}}</label>
 					<input id="password" name="password" type="password" autocomplete="new-password" value="{{.password}}" {{if eq .login_type "0-0"}}required{{end}}>
 				</div>
 
-				<div class="inline field local {{if ne .login_type "0-0"}}gt-hidden{{end}}">
+				<div class="inline field local {{if ne .login_type "0-0"}}tw-hidden{{end}}">
 					<div class="ui checkbox">
 						<label><strong>{{ctx.Locale.Tr "auth.allow_password_change"}}</strong></label>
 						<input name="must_change_password" type="checkbox" checked>
diff --git a/templates/admin/user/view.tmpl b/templates/admin/user/view.tmpl
index fd3017607c..21943a8382 100644
--- a/templates/admin/user/view.tmpl
+++ b/templates/admin/user/view.tmpl
@@ -2,7 +2,7 @@
 
 <div class="admin-setting-content">
 	<div class="admin-responsive-columns">
-		<div class="gt-f1">
+		<div class="tw-flex-1">
 			<h4 class="ui top attached header">
 				{{.Title}}
 				<div class="ui right">
@@ -13,7 +13,7 @@
 				{{template "admin/user/view_details" .}}
 			</div>
 		</div>
-		<div class="gt-f1">
+		<div class="tw-flex-1">
 			<h4 class="ui top attached header">
 				{{ctx.Locale.Tr "admin.emails"}}
 				<div class="ui right">
diff --git a/templates/admin/user/view_details.tmpl b/templates/admin/user/view_details.tmpl
index 21425eecb4..be2f32b5ec 100644
--- a/templates/admin/user/view_details.tmpl
+++ b/templates/admin/user/view_details.tmpl
@@ -48,6 +48,14 @@
 					{{svg "octicon-x"}}
 				{{end}}
 			</div>
+			{{if .User.Language}}
+				<div class="flex-item-body">
+					<span class="flex-text-inline">
+						<b>{{ctx.Locale.Tr "settings.language"}}:</b>
+						{{range .AllLangs}}{{if eq $.User.Language .Lang}}{{.Name}}{{end}}{{end}}
+					</span>
+				</div>
+			{{end}}
 			{{if .User.Location}}
 				<div class="flex-item-body">
 					<span class="flex-text-inline">{{svg "octicon-location"}}{{.User.Location}}</span>
diff --git a/templates/base/alert.tmpl b/templates/base/alert.tmpl
index 160584f769..760d3bfa2c 100644
--- a/templates/base/alert.tmpl
+++ b/templates/base/alert.tmpl
@@ -1,20 +1,20 @@
 {{if .Flash.ErrorMsg}}
 	<div class="ui negative message flash-message flash-error">
-		<p>{{.Flash.ErrorMsg | Str2html}}</p>
+		<p>{{.Flash.ErrorMsg | SanitizeHTML}}</p>
 	</div>
 {{end}}
 {{if .Flash.SuccessMsg}}
 	<div class="ui positive message flash-message flash-success">
-		<p>{{.Flash.SuccessMsg | Str2html}}</p>
+		<p>{{.Flash.SuccessMsg | SanitizeHTML}}</p>
 	</div>
 {{end}}
 {{if .Flash.InfoMsg}}
 	<div class="ui info message flash-message flash-info">
-		<p>{{.Flash.InfoMsg | Str2html}}</p>
+		<p>{{.Flash.InfoMsg | SanitizeHTML}}</p>
 	</div>
 {{end}}
 {{if .Flash.WarningMsg}}
 	<div class="ui warning message flash-message flash-warning">
-		<p>{{.Flash.WarningMsg | Str2html}}</p>
+		<p>{{.Flash.WarningMsg | SanitizeHTML}}</p>
 	</div>
 {{end}}
diff --git a/templates/base/alert_details.tmpl b/templates/base/alert_details.tmpl
index 1d7ec15dc0..6801c8240f 100644
--- a/templates/base/alert_details.tmpl
+++ b/templates/base/alert_details.tmpl
@@ -2,6 +2,6 @@
 <details>
 	<summary>{{.Summary}}</summary>
 	<code>
-		{{.Details | Str2html}}
+		{{.Details | SanitizeHTML}}
 	</code>
 </details>
diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl
index d65a3626a4..fed426a469 100644
--- a/templates/base/footer.tmpl
+++ b/templates/base/footer.tmpl
@@ -16,6 +16,5 @@
 	<script src="{{AssetUrlPrefix}}/js/index.js?v={{AssetVersion}}" onerror="alert('Failed to load asset files from ' + this.src + '. Please make sure the asset files can be accessed.')"></script>
 
 	{{template "custom/footer" .}}
-	{{ctx.DataRaceCheck $.Context}}
 </body>
 </html>
diff --git a/templates/base/footer_content.tmpl b/templates/base/footer_content.tmpl
index f0a7865602..8d0d8e669c 100644
--- a/templates/base/footer_content.tmpl
+++ b/templates/base/footer_content.tmpl
@@ -1,6 +1,8 @@
 <footer class="page-footer" role="group" aria-label="{{ctx.Locale.Tr "aria.footer"}}">
 	<div class="left-links" role="contentinfo" aria-label="{{ctx.Locale.Tr "aria.footer.software"}}">
-		<a target="_blank" rel="noopener noreferrer" href="https://about.gitea.com">{{ctx.Locale.Tr "powered_by" "Gitea"}}</a>
+		{{if ShowFooterPoweredBy}}
+			<a target="_blank" rel="noopener noreferrer" href="https://about.gitea.com">{{ctx.Locale.Tr "powered_by" "Gitea"}}</a>
+		{{end}}
 		{{if (or .ShowFooterVersion .PageIsAdmin)}}
 			{{ctx.Locale.Tr "version"}}:
 			{{if .IsAdmin}}
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index b9c050fdd5..2de8f58235 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -2,7 +2,7 @@
 <html lang="{{ctx.Locale.Lang}}" data-theme="{{ThemeName .SignedUser}}">
 <head>
 	<meta name="viewport" content="width=device-width, initial-scale=1">
-	<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title>
+	<title>{{if .Title}}{{.Title}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title>
 	{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
 	<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">
 	<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
@@ -29,8 +29,7 @@
 	{{template "base/head_style" .}}
 	{{template "custom/header" .}}
 </head>
-<body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-push-url="false">
-	{{ctx.DataRaceCheck $.Context}}
+<body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-ext="morph" hx-push-url="false">
 	{{template "custom/body_outer_pre" .}}
 
 	<div class="full height">
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index effe4dcea9..addff22c49 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -11,16 +11,16 @@
 		</a>
 
 		<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
-		<div class="ui secondary menu item navbar-mobile-right">
+		<div class="ui secondary menu item navbar-mobile-right only-mobile">
 			{{if .IsSigned}}
-			<a id="mobile-notifications-icon" class="item gt-w-auto gt-p-3" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
-				<div class="gt-relative">
+			<a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
+				<div class="tw-relative">
 					{{svg "octicon-bell"}}
-					<span class="notification_count{{if not $notificationUnreadCount}} gt-hidden{{end}}">{{$notificationUnreadCount}}</span>
+					<span class="notification_count{{if not $notificationUnreadCount}} tw-hidden{{end}}">{{$notificationUnreadCount}}</span>
 				</div>
 			</a>
 			{{end}}
-			<button class="item gt-w-auto ui icon mini button gt-p-3 gt-m-0" id="navbar-expand-toggle">{{svg "octicon-three-bars"}}</button>
+			<button class="item tw-w-auto ui icon mini button tw-p-2 tw-m-0" id="navbar-expand-toggle" aria-label="{{ctx.Locale.Tr "home.nav_menu"}}">{{svg "octicon-three-bars"}}</button>
 		</div>
 
 		<!-- navbar links non-mobile -->
@@ -56,9 +56,9 @@
 	<div class="navbar-right ui secondary menu">
 		{{if and .IsSigned .MustChangePassword}}
 			<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
-				<span class="text gt-df gt-ac">
-					{{ctx.AvatarUtils.Avatar .SignedUser 24 "gt-mr-2"}}
-					<span class="mobile-only gt-ml-3">{{.SignedUser.Name}}</span>
+				<span class="text tw-flex tw-items-center">
+					{{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}}
+					<span class="only-mobile tw-ml-2">{{.SignedUser.Name}}</span>
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
 				</span>
 				<div class="menu user-menu">
@@ -75,19 +75,19 @@
 			</div><!-- end dropdown avatar menu -->
 		{{else if .IsSigned}}
 			{{if EnableTimetracking}}
-			<a class="active-stopwatch-trigger item gt-mx-0{{if not .ActiveStopwatch}} gt-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}">
-				<div class="gt-relative">
+			<a class="active-stopwatch-trigger item tw-mx-0{{if not .ActiveStopwatch}} tw-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}">
+				<div class="tw-relative">
 					{{svg "octicon-stopwatch"}}
 					<span class="header-stopwatch-dot"></span>
 				</div>
-				<span class="mobile-only gt-ml-3">{{ctx.Locale.Tr "active_stopwatch"}}</span>
+				<span class="only-mobile tw-ml-2">{{ctx.Locale.Tr "active_stopwatch"}}</span>
 			</a>
-			<div class="active-stopwatch-popup item tippy-target gt-p-3">
-				<div class="gt-df gt-ac">
-					<a class="stopwatch-link gt-df gt-ac" href="{{.ActiveStopwatch.IssueLink}}">
-						{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
+			<div class="active-stopwatch-popup item tippy-target tw-p-2">
+				<div class="tw-flex tw-items-center">
+					<a class="stopwatch-link tw-flex tw-items-center" href="{{.ActiveStopwatch.IssueLink}}">
+						{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
 						<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
-						<span class="ui primary label stopwatch-time gt-my-0 gt-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
+						<span class="ui primary label stopwatch-time tw-my-0 tw-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
 							{{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}}
 						</span>
 					</a>
@@ -111,18 +111,18 @@
 			</div>
 			{{end}}
 
-			<a class="item not-mobile gt-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
-				<div class="gt-relative">
+			<a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
+				<div class="tw-relative">
 					{{svg "octicon-bell"}}
-					<span class="notification_count{{if not $notificationUnreadCount}} gt-hidden{{end}}">{{$notificationUnreadCount}}</span>
+					<span class="notification_count{{if not $notificationUnreadCount}} tw-hidden{{end}}">{{$notificationUnreadCount}}</span>
 				</div>
 			</a>
 
-			<div class="ui dropdown jump item gt-mx-0 gt-pr-3" data-tooltip-content="{{ctx.Locale.Tr "create_new"}}">
+			<div class="ui dropdown jump item tw-mx-0 tw-pr-2" data-tooltip-content="{{ctx.Locale.Tr "create_new"}}">
 				<span class="text">
 					{{svg "octicon-plus"}}
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
-					<span class="mobile-only">{{ctx.Locale.Tr "create_new"}}</span>
+					<span class="only-mobile">{{ctx.Locale.Tr "create_new"}}</span>
 				</span>
 				<div class="menu">
 					<a class="item" href="{{AppSubUrl}}/repo/create">
@@ -141,10 +141,10 @@
 				</div><!-- end content create new menu -->
 			</div><!-- end dropdown menu create new -->
 
-			<div class="ui dropdown jump item gt-mx-0 gt-pr-3" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
-				<span class="text gt-df gt-ac">
-					{{ctx.AvatarUtils.Avatar .SignedUser 24 "gt-mr-2"}}
-					<span class="mobile-only gt-ml-3">{{.SignedUser.Name}}</span>
+			<div class="ui dropdown jump item tw-mx-0 tw-pr-2" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
+				<span class="text tw-flex tw-items-center">
+					{{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}}
+					<span class="only-mobile tw-ml-2">{{.SignedUser.Name}}</span>
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
 				</span>
 				<div class="menu user-menu">
diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl
index 4a723f63b9..22e08e9c8f 100644
--- a/templates/base/head_script.tmpl
+++ b/templates/base/head_script.tmpl
@@ -41,6 +41,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
 			remove_label_str: {{ctx.Locale.Tr "remove_label_str"}},
 			modal_confirm: {{ctx.Locale.Tr "modal.confirm"}},
 			modal_cancel: {{ctx.Locale.Tr "modal.cancel"}},
+			more_items: {{ctx.Locale.Tr "more_items"}},
 		},
 	};
 	{{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}}
diff --git a/templates/base/markup_codepreview.tmpl b/templates/base/markup_codepreview.tmpl
new file mode 100644
index 0000000000..a1a4f26b47
--- /dev/null
+++ b/templates/base/markup_codepreview.tmpl
@@ -0,0 +1,25 @@
+<div class="code-preview-container file-content">
+	<div class="code-preview-header">
+		<a href="{{.FullURL}}" class="muted" rel="nofollow">{{.FilePath}}</a>
+		{{$link := HTMLFormat `<a href="%s/src/commit/%s" rel="nofollow">%s</a>` .RepoLink .CommitID (.CommitID | ShortSha) -}}
+		{{- if eq .LineStart .LineStop -}}
+			{{ctx.Locale.Tr "repo.code_preview_line_in" .LineStart $link}}
+		{{- else -}}
+			{{ctx.Locale.Tr "repo.code_preview_line_from_to" .LineStart .LineStop $link}}
+		{{- end}}
+	</div>
+	<table class="file-view">
+		<tbody>
+			{{- range $idx, $line := .HighlightLines -}}
+			<tr>
+				<td class="lines-num"><span data-line-number="{{$line.Num}}"></span></td>
+				{{- if $.EscapeStatus.Escaped -}}
+					{{- $lineEscapeStatus := index $.LineEscapeStatus $idx -}}
+					<td class="lines-escape">{{if $lineEscapeStatus.Escaped}}<a href="#" class="toggle-escape-button btn interact-bg" title="{{if $lineEscapeStatus.HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end}}{{if $lineEscapeStatus.HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}}"></a>{{end}}</td>
+				{{- end}}
+				<td class="lines-code chroma"><div class="code-inner">{{$line.FormattedContent}}</div></td>{{/* only div works, span generates incorrect HTML structure */}}
+			</tr>
+			{{- end -}}
+		</tbody>
+	</table>
+</div>
diff --git a/templates/base/paginate.tmpl b/templates/base/paginate.tmpl
index 503817c339..9a7a6322f7 100644
--- a/templates/base/paginate.tmpl
+++ b/templates/base/paginate.tmpl
@@ -1,30 +1,32 @@
-{{$paginationLink := .Page.GetParams}}
+{{$paginationParams := .Page.GetParams}}
+{{$paginationLink := $.Link}}
+{{if eq $paginationLink AppSubUrl}}{{$paginationLink = print $paginationLink "/"}}{{end}}
 {{with .Page.Paginater}}
 	{{if gt .TotalPages 1}}
 		<div class="center page buttons">
 			<div class="ui borderless pagination menu">
-				<a class="{{if .IsFirst}}disabled{{end}} item navigation" {{if not .IsFirst}}href="{{$.Link}}{{if $paginationLink}}?{{$paginationLink}}{{end}}"{{end}}>
-					{{svg "gitea-double-chevron-left" 16 "gt-mr-2"}}
+				<a class="{{if .IsFirst}}disabled{{end}} item navigation" {{if not .IsFirst}}href="{{$paginationLink}}{{if $paginationParams}}?{{$paginationParams}}{{end}}"{{end}}>
+					{{svg "gitea-double-chevron-left" 16 "tw-mr-1"}}
 					<span class="navigation_label">{{ctx.Locale.Tr "admin.first_page"}}</span>
 				</a>
-				<a class="{{if not .HasPrevious}}disabled{{end}} item navigation" {{if .HasPrevious}}href="{{$.Link}}?page={{.Previous}}{{if $paginationLink}}&{{$paginationLink}}{{end}}"{{end}}>
-					{{svg "octicon-chevron-left" 16 "gt-mr-2"}}
+				<a class="{{if not .HasPrevious}}disabled{{end}} item navigation" {{if .HasPrevious}}href="{{$paginationLink}}?page={{.Previous}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>
+					{{svg "octicon-chevron-left" 16 "tw-mr-1"}}
 					<span class="navigation_label">{{ctx.Locale.Tr "repo.issues.previous"}}</span>
 				</a>
 				{{range .Pages}}
 					{{if eq .Num -1}}
 						<a class="disabled item">...</a>
 					{{else}}
-						<a class="{{if .IsCurrent}}active {{end}}item gt-content-center" {{if not .IsCurrent}}href="{{$.Link}}?page={{.Num}}{{if $paginationLink}}&{{$paginationLink}}{{end}}"{{end}}>{{.Num}}</a>
+						<a class="{{if .IsCurrent}}active {{end}}item tw-content-center" {{if not .IsCurrent}}href="{{$paginationLink}}?page={{.Num}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>{{.Num}}</a>
 					{{end}}
 				{{end}}
-				<a class="{{if not .HasNext}}disabled{{end}} item navigation" {{if .HasNext}}href="{{$.Link}}?page={{.Next}}{{if $paginationLink}}&{{$paginationLink}}{{end}}"{{end}}>
+				<a class="{{if not .HasNext}}disabled{{end}} item navigation" {{if .HasNext}}href="{{$paginationLink}}?page={{.Next}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>
 					<span class="navigation_label">{{ctx.Locale.Tr "repo.issues.next"}}</span>
-					{{svg "octicon-chevron-right" 16 "gt-ml-2"}}
+					{{svg "octicon-chevron-right" 16 "tw-ml-1"}}
 				</a>
-				<a class="{{if .IsLast}}disabled{{end}} item navigation" {{if not .IsLast}}href="{{$.Link}}?page={{.TotalPages}}{{if $paginationLink}}&{{$paginationLink}}{{end}}"{{end}}>
+				<a class="{{if .IsLast}}disabled{{end}} item navigation" {{if not .IsLast}}href="{{$paginationLink}}?page={{.TotalPages}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>
 					<span class="navigation_label">{{ctx.Locale.Tr "admin.last_page"}}</span>
-					{{svg "gitea-double-chevron-right" 16 "gt-ml-2"}}
+					{{svg "gitea-double-chevron-right" 16 "tw-ml-1"}}
 				</a>
 			</div>
 		</div>
diff --git a/templates/code/searchcombo.tmpl b/templates/code/searchcombo.tmpl
deleted file mode 100644
index 48dc13b47b..0000000000
--- a/templates/code/searchcombo.tmpl
+++ /dev/null
@@ -1,17 +0,0 @@
-{{template "code/searchform" .}}
-<div class="divider"></div>
-<div class="ui user list">
-	{{if .CodeIndexerUnavailable}}
-		<div class="ui error message">
-			<p>{{ctx.Locale.Tr "explore.code_search_unavailable"}}</p>
-		</div>
-	{{else if .SearchResults}}
-		<h3>
-			{{ctx.Locale.Tr "explore.code_search_results" (.Keyword|Escape) | Str2html}}
-		</h3>
-		{{template "code/searchresults" .}}
-	{{else if .Keyword}}
-		<div>{{ctx.Locale.Tr "explore.code_no_results"}}</div>
-	{{end}}
-</div>
-{{template "base/paginate" .}}
diff --git a/templates/code/searchform.tmpl b/templates/code/searchform.tmpl
deleted file mode 100644
index fae1340046..0000000000
--- a/templates/code/searchform.tmpl
+++ /dev/null
@@ -1,14 +0,0 @@
-<form class="ui form ignore-dirty">
-	<div class="ui fluid action input">
-		{{template "shared/searchinput" dict "Value" .Keyword "Disabled" .CodeIndexerUnavailable}}
-		<div class="ui dropdown selection {{if .CodeIndexerUnavailable}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "explore.search.type.tooltip"}}">
-			<input name="t" type="hidden" value="{{.queryType}}"{{if .CodeIndexerUnavailable}} disabled{{end}}>{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-			<div class="text">{{ctx.Locale.Tr (printf "explore.search.%s" (or .queryType "fuzzy"))}}</div>
-			<div class="menu">
-				<div class="item" data-value="" data-tooltip-content="{{ctx.Locale.Tr "explore.search.fuzzy.tooltip"}}">{{ctx.Locale.Tr "explore.search.fuzzy"}}</div>
-				<div class="item" data-value="match" data-tooltip-content="{{ctx.Locale.Tr "explore.search.match.tooltip"}}">{{ctx.Locale.Tr "explore.search.match"}}</div>
-			</div>
-		</div>
-		<button class="ui primary button"{{if .CodeIndexerUnavailable}} disabled{{end}}>{{ctx.Locale.Tr "explore.search"}}</button>
-	</div>
-</form>
diff --git a/templates/code/searchresults.tmpl b/templates/code/searchresults.tmpl
deleted file mode 100644
index bb21a5e0dc..0000000000
--- a/templates/code/searchresults.tmpl
+++ /dev/null
@@ -1,43 +0,0 @@
-<div class="flex-text-block gt-fw">
-	{{range $term := .SearchResultLanguages}}
-	<a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label gt-m-0" href="{{AppSubUrl}}{{if $.ContextUser}}/{{$.ContextUser.Name}}/-/code{{else}}/explore/code{{end}}?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}{{if ne $.queryType ""}}&t={{$.queryType}}{{end}}">
-		<i class="color-icon gt-mr-3" style="background-color: {{$term.Color}}"></i>
-		{{$term.Language}}
-		<div class="detail">{{$term.Count}}</div>
-	</a>
-	{{end}}
-</div>
-<div class="repository search">
-	{{range $result := .SearchResults}}
-		{{$repo := (index $.RepoMaps .RepoID)}}
-		<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
-			<h4 class="ui top attached normal header gt-df gt-fw">
-				<span class="file gt-f1">
-					<a rel="nofollow" href="{{$repo.Link}}">{{$repo.FullName}}</a>
-						{{if $repo.IsArchived}}
-							<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.archived"}}</span>
-						{{end}}
-					- {{.Filename}}
-				</span>
-				<a role="button" class="ui basic tiny button" rel="nofollow" href="{{$repo.Link}}/src/commit/{{$result.CommitID | PathEscape}}/{{.Filename | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
-			</h4>
-			<div class="ui attached table segment">
-				<div class="file-body file-code code-view">
-					<table>
-						<tbody>
-							<tr>
-								<td class="lines-num">
-									{{range .LineNumbers}}
-										<a href="{{$repo.Link}}/src/commit/{{$result.CommitID | PathEscape}}/{{$result.Filename | PathEscapeSegments}}#L{{.}}"><span>{{.}}</span></a>
-									{{end}}
-								</td>
-								<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines}}</code></td>
-							</tr>
-						</tbody>
-					</table>
-				</div>
-			</div>
-			{{template "shared/searchbottom" dict "root" $ "result" .}}
-		</div>
-	{{end}}
-</div>
diff --git a/templates/devtest/fetch-action.tmpl b/templates/devtest/fetch-action.tmpl
index 70844a8751..7b0bbba554 100644
--- a/templates/devtest/fetch-action.tmpl
+++ b/templates/devtest/fetch-action.tmpl
@@ -26,7 +26,7 @@
 				<div><button name="btn">submit post</button></div>
 			</form>
 			<form method="post" action="/no-such-uri" class="form-fetch-action">
-				<div class="gt-py-5">bad action url</div>
+				<div class="tw-py-8">bad action url</div>
 				<div><button name="btn">submit test</button></div>
 			</form>
 		</div>
diff --git a/templates/devtest/flex-list.tmpl b/templates/devtest/flex-list.tmpl
index c8584c110b..015ab1e154 100644
--- a/templates/devtest/flex-list.tmpl
+++ b/templates/devtest/flex-list.tmpl
@@ -25,7 +25,7 @@
 				</div>
 				<div class="flex-item-trailing">
 					<button class="ui tiny red button">
-						{{svg "octicon-warning" 14}} CJK文本测试
+						{{svg "octicon-alert" 14}} CJK文本测试
 					</button>
 					<button class="ui tiny primary button">
 						{{svg "octicon-info" 14}} Button
@@ -54,7 +54,7 @@
 				</div>
 				<div class="flex-item-trailing">
 					<button class="ui tiny red button">
-						{{svg "octicon-warning" 12}} CJK文本测试 <!-- single CJK text test, it shouldn't be horizontal -->
+						{{svg "octicon-alert" 12}} CJK文本测试 <!-- single CJK text test, it shouldn't be horizontal -->
 					</button>
 				</div>
 			</div>
@@ -73,7 +73,7 @@
 						</div>
 						<div class="flex-item-trailing">
 							<a class="muted" href="{{$.Link}}">
-								<span class="flex-text-inline"><i class="color-icon gt-mr-3" style="background-color: aqua"></i>Go</span>
+								<span class="flex-text-inline"><i class="color-icon tw-mr-2 tw-bg-blue"></i>Go</span>
 							</a>
 							<a class="text grey flex-text-inline" href="{{$.Link}}">{{svg "octicon-star" 16}}45000</a>
 							<a class="text grey flex-text-inline" href="{{$.Link}}">{{svg "octicon-git-branch" 16}}1234</a>
@@ -104,7 +104,7 @@
 		</div>
 
 		<h1>If parent provides the padding/margin space:</h1>
-		<div class="gt-border-secondary gt-py-4">
+		<div class="tw-border tw-border-secondary tw-py-4">
 			<div class="flex-list flex-space-fitted">
 				<div class="flex-item">item 1 (no padding top)</div>
 				<div class="flex-item">item 2 (no padding bottom)</div>
diff --git a/templates/devtest/fomantic-modal.tmpl b/templates/devtest/fomantic-modal.tmpl
index eda169a043..5cd36721a7 100644
--- a/templates/devtest/fomantic-modal.tmpl
+++ b/templates/devtest/fomantic-modal.tmpl
@@ -5,7 +5,7 @@
 	<div id="test-modal-form-1" class="ui mini modal">
 		<div class="header">Form dialog (layout 1)</div>
 		<form class="content" method="post">
-			<div class="ui input gt-w-full"><input name="user_input"></div>
+			<div class="ui input tw-w-full"><input name="user_input"></div>
 			{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
 		</form>
 	</div>
@@ -14,7 +14,7 @@
 		<div class="header">Form dialog (layout 2)</div>
 		<form method="post">
 			<div class="content">
-				<div class="ui input gt-w-full"><input name="user_input"></div>
+				<div class="ui input tw-w-full"><input name="user_input"></div>
 				{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
 			</div>
 		</form>
@@ -24,7 +24,7 @@
 		<div class="header">Form dialog (layout 3)</div>
 		<form method="post">
 			<div class="content">
-				<div class="ui input gt-w-full"><input name="user_input"></div>
+				<div class="ui input tw-w-full"><input name="user_input"></div>
 			</div>
 			{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
 		</form>
@@ -33,7 +33,7 @@
 	<div id="test-modal-form-4" class="ui mini modal">
 		<div class="header">Form dialog (layout 4)</div>
 		<div class="content">
-			<div class="ui input gt-w-full"><input name="user_input"></div>
+			<div class="ui input tw-w-full"><input name="user_input"></div>
 		</div>
 		<form method="post">
 			{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
@@ -73,7 +73,7 @@
 		{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" "I know and must do  this is dangerous operation")}}
 	</div>
 
-	<div class="modal-buttons flex-text-block gt-fw"></div>
+	<div class="modal-buttons flex-text-block tw-flex-wrap"></div>
 	<script type="module">
 		for (const el of $('.ui.modal')) {
 			const $btn = $('<button>').text(`${el.id}`).on('click', () => {
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
index 73293ddf48..bb4fc77a74 100644
--- a/templates/devtest/gitea-ui.tmpl
+++ b/templates/devtest/gitea-ui.tmpl
@@ -2,6 +2,13 @@
 <link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}">
 <div class="page-content devtest ui container">
 	<div>
+		<h1>Link</h1>
+		<div>
+			<a href="#">normal</a>
+			<a class="muted" href="#">muted</a>
+			<a class="suppressed" href="#">suppressed</a>
+			<a class="silenced" href="#">silenced</a>
+		</div>
 		<h1>Button</h1>
 		<div>
 			Style:
@@ -60,10 +67,10 @@
 				</li>
 				<li class="sample-group">
 					<h2>Inline / Plain:</h2>
-					<div class="gt-my-2">
-						<button class="btn gt-p-3">Plain button</button>
-						<button class="btn interact-fg gt-p-3">Plain button with interact fg</button>
-						<button class="btn interact-bg gt-p-3">Plain button with interact bg</button>
+					<div class="tw-my-1">
+						<button class="btn tw-p-2">Plain button</button>
+						<button class="btn interact-fg tw-p-2">Plain button with interact fg</button>
+						<button class="btn interact-bg tw-p-2">Plain button with interact bg</button>
 					</div>
 				</li>
 			</ul>
@@ -95,8 +102,8 @@
 
 	<div>
 		<h1>Loading</h1>
-		<div class="is-loading small-loading-icon gt-border-secondary gt-py-2"><span>loading ...</span></div>
-		<div class="is-loading gt-border-secondary gt-py-4">
+		<div class="is-loading loading-icon-2px tw-border tw-border-secondary tw-py-1"><span>loading ...</span></div>
+		<div class="is-loading tw-border tw-border-secondary tw-py-4">
 			<p>loading ...</p>
 			<p>loading ...</p>
 			<p>loading ...</p>
@@ -105,9 +112,46 @@
 	</div>
 
 	<div>
-		<h1>GiteaOriginUrl</h1>
-		<div><gitea-origin-url data-url="test/url"></gitea-origin-url></div>
-		<div><gitea-origin-url data-url="/test/url"></gitea-origin-url></div>
+		<h1>&lt;origin-url&gt;</h1>
+		<div><origin-url data-url="test/url"></origin-url></div>
+		<div><origin-url data-url="/test/url"></origin-url></div>
+	</div>
+
+	<div>
+		<h1>&lt;overflow-menu&gt;</h1>
+		<overflow-menu class="ui secondary pointing tabular borderless menu">
+			<div class="overflow-menu-items">
+				<a class="active item">item</a>
+				<a class="item">item 1</a>
+				<a class="item">item 2</a>
+				<a class="item">item 3</a>
+				<a class="item">item 4</a>
+				<a class="item">item 5</a>
+				<a class="item">item 6</a>
+				<a class="item">item 7</a>
+				<a class="item">item 8</a>
+				<a class="item">item 9</a>
+				<a class="item">item 10</a>
+				<a class="item">item 11</a>
+				<a class="item">item 12</a>
+				<a class="item">item 13</a>
+				<a class="item">item 14</a>
+				<a class="item">item 15</a>
+				<a class="item">item 16</a>
+				<a class="item">item 17</a>
+				<a class="item">item 18</a>
+			</div>
+		</overflow-menu>
+	</div>
+
+	<div>
+		<h1>GiteaAbsoluteDate</h1>
+		<div><absolute-date date="2024-03-11" year="numeric" day="numeric" month="short"></absolute-date></div>
+		<div><absolute-date date="2024-03-11" year="numeric" day="numeric" month="long"></absolute-date></div>
+		<div><absolute-date date="2024-03-11" year="" day="numeric" month="numeric"></absolute-date></div>
+		<div><absolute-date date="2024-03-11" year="" day="numeric" month="numeric" weekday="long"></absolute-date></div>
+		<div><absolute-date date="2024-03-11T19:00:00-05:00" year="" day="numeric" month="numeric" weekday="long"></absolute-date></div>
+		<div class="tw-text-text-light-2">relative-time: <relative-time format="datetime" datetime="2024-03-11" year="" day="numeric" month="numeric"></relative-time></div>
 	</div>
 
 	<div>
@@ -167,12 +211,13 @@
 
 		<h2>Dropdown with SVG</h2>
 		<div>
-			<div class="ui dropdown" style="border: 1px red dashed" data-tooltip-content="border for demo purpose only">
-				<span class="text">simple</span>
+			<div class="ui dropdown tw-border tw-border-red tw-border-dashed" data-tooltip-content="border for demo purpose only">
+				<span class="text">search-input &amp; flex-item in menu</span>
 				{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-				<div class="menu">
+				<div class="menu flex-items-menu">
 					<div class="ui icon search input"><i class="icon">{{svg "octicon-search"}}</i><input type="text" value="search input in menu"></div>
-					<div class="item">item</div>
+					<div class="item"><input type="radio">item</div>
+					<div class="item"><input type="radio">item</div>
 				</div>
 			</div>
 			<div class="ui search selection dropdown">
@@ -275,6 +320,12 @@
 		<div>ps: no JS code attached, so just a layout</div>
 		{{template "shared/combomarkdowneditor" .}}
 	</div>
+
+	<h1>Tailwind CSS Demo</h1>
+	<div>
+		<button class="{{if true}}tw-bg-red{{end}} tw-p-5 tw-border tw-rounded hover:tw-bg-blue active:tw-bg-yellow">Button</button>
+	</div>
+
 	<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>
 </div>
 {{template "base/footer" .}}
diff --git a/templates/devtest/tmplerr.tmpl b/templates/devtest/tmplerr.tmpl
index 2fe3f1effd..dd938c895e 100644
--- a/templates/devtest/tmplerr.tmpl
+++ b/templates/devtest/tmplerr.tmpl
@@ -1,10 +1,10 @@
 {{template "base/head" .}}
 <div class="page-content devtest">
-	<div class="gt-df">
-		<div style="width: 80%; ">
+	<div class="tw-flex">
+		<div class="tw-w-4/5">
 			hello hello hello hello hello hello hello hello hello hello
 		</div>
-		<div style="width: 20%;">
+		<div class="tw-w-1/5">
 			{{template "devtest/tmplerr-sub" .}}
 		</div>
 	</div>
diff --git a/templates/explore/code.tmpl b/templates/explore/code.tmpl
index 2298575887..039933fa2d 100644
--- a/templates/explore/code.tmpl
+++ b/templates/explore/code.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content explore users">
 	{{template "explore/navbar" .}}
 	<div class="ui container">
-		{{template "code/searchcombo" .}}
+		{{template "shared/search/code/search" .}}
 	</div>
 </div>
 {{template "base/footer" .}}
diff --git a/templates/explore/navbar.tmpl b/templates/explore/navbar.tmpl
index 7f2aea497a..8e619fa66f 100644
--- a/templates/explore/navbar.tmpl
+++ b/templates/explore/navbar.tmpl
@@ -1,5 +1,5 @@
-<div class="ui secondary pointing tabular top attached borderless menu new-menu navbar">
-	<div class="new-menu-inner">
+<overflow-menu class="ui secondary pointing tabular top attached borderless menu secondary-nav">
+	<div class="overflow-menu-items tw-justify-center">
 		<a class="{{if .PageIsExploreRepositories}}active {{end}}item" href="{{AppSubUrl}}/explore/repos">
 			{{svg "octicon-repo"}} {{ctx.Locale.Tr "explore.repos"}}
 		</a>
@@ -17,4 +17,4 @@
 		</a>
 		{{end}}
 	</div>
-</div>
+</overflow-menu>
diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl
index c51dcaa3ff..d00773a963 100644
--- a/templates/explore/repo_list.tmpl
+++ b/templates/explore/repo_list.tmpl
@@ -30,16 +30,23 @@
 							{{end}}
 						</span>
 					</div>
-					<div class="flex-item-trailing">
+					<div class="flex-item-trailing muted-links">
 						{{if .PrimaryLanguage}}
-							<a class="muted" href="{{$.Link}}?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}{{if $.TabName}}&tab={{$.TabName}}{{end}}">
-								<span class="flex-text-inline"><i class="color-icon gt-mr-3" style="background-color: {{.PrimaryLanguage.Color}}"></i>{{.PrimaryLanguage.Language}}</span>
+							<a class="flex-text-inline" href="?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}{{if $.TabName}}&tab={{$.TabName}}{{end}}">
+								<i class="color-icon tw-mr-2" style="background-color: {{.PrimaryLanguage.Color}}"></i>
+								{{.PrimaryLanguage.Language}}
 							</a>
 						{{end}}
 						{{if not $.DisableStars}}
-							<a class="text grey flex-text-inline" href="{{.Link}}/stars">{{svg "octicon-star" 16}}{{.NumStars}}</a>
+							<a class="flex-text-inline" href="{{.Link}}/stars">
+								<span aria-label="{{ctx.Locale.Tr "repo.stars"}}">{{svg "octicon-star" 16}}</span>
+								<span {{if ge .NumStars 1000}}data-tooltip-content="{{.NumStars}}"{{end}}>{{CountFmt .NumStars}}</span>
+							</a>
 						{{end}}
-						<a class="text grey flex-text-inline" href="{{.Link}}/forks">{{svg "octicon-git-branch" 16}}{{.NumForks}}</a>
+						<a class="flex-text-inline" href="{{.Link}}/forks">
+							<span aria-label="{{ctx.Locale.Tr "repo.forks"}}">{{svg "octicon-git-branch" 16}}</span>
+							<span {{if ge .NumForks 1000}}data-tooltip-content="{{.NumForks}}"{{end}}>{{CountFmt .NumForks}}</span>
+						</a>
 					</div>
 				</div>
 				{{$description := .DescriptionHTML $.Context}}
@@ -58,7 +65,7 @@
 		</div>
 	{{else}}
 	<div>
-		{{ctx.Locale.Tr "explore.repo_no_results"}}
+		{{ctx.Locale.Tr "search.no_results"}}
 	</div>
 	{{end}}
 </div>
diff --git a/templates/explore/repo_search.tmpl b/templates/explore/repo_search.tmpl
deleted file mode 100644
index eaf2e7a090..0000000000
--- a/templates/explore/repo_search.tmpl
+++ /dev/null
@@ -1,42 +0,0 @@
-<div class="ui secondary filter menu gt-ac gt-mx-0">
-	<form class="ui form ignore-dirty gt-f1">
-		<input type="hidden" name="sort" value="{{$.SortType}}">
-		<input type="hidden" name="language" value="{{$.Language}}">
-		<div class="ui fluid action input">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
-			{{if .PageIsExploreRepositories}}
-				<input type="hidden" name="only_show_relevant" value="{{.OnlyShowRelevant}}">
-			{{else if .TabName}}
-				<input type="hidden" name="tab" value="{{.TabName}}">
-			{{end}}
-			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-		</div>
-	</form>
-	<!-- Sort -->
-	<div class="ui dropdown type jump item gt-mr-0">
-		<span class="text">
-			{{ctx.Locale.Tr "repo.issues.filter_sort"}}
-		</span>
-		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-		<div class="menu">
-			<a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=newest&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-			<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=oldest&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=alphabetically&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=reversealphabetically&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=recentupdate&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=leastupdate&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-			{{if not .DisableStars}}
-				<a class="{{if eq .SortType "moststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=moststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</a>
-				<a class="{{if eq .SortType "feweststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=feweststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</a>
-			{{end}}
-			<a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=mostforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
-			<a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=fewestforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
-		</div>
-	</div>
-</div>
-{{if and .PageIsExploreRepositories .OnlyShowRelevant}}
-	<div class="ui message explore-relevancy-note">
-		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">{{ctx.Locale.Tr "explore.relevant_repositories" ((printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))|Escape) | Safe}}</span>
-	</div>
-{{end}}
-<div class="divider"></div>
diff --git a/templates/explore/repos.tmpl b/templates/explore/repos.tmpl
index dfede2ffcc..53742bf0d9 100644
--- a/templates/explore/repos.tmpl
+++ b/templates/explore/repos.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content explore repositories">
 	{{template "explore/navbar" .}}
 	<div class="ui container">
-		{{template "explore/repo_search" .}}
+		{{template "shared/repo_search" .}}
 		{{template "explore/repo_list" .}}
 		{{template "base/paginate" .}}
 	</div>
diff --git a/templates/explore/search.tmpl b/templates/explore/search.tmpl
index 74b80436dc..1d984a2e37 100644
--- a/templates/explore/search.tmpl
+++ b/templates/explore/search.tmpl
@@ -1,23 +1,22 @@
-<div class="ui secondary filter menu gt-ac gt-mx-0">
-	<form class="ui form ignore-dirty gt-f1">
-		<div class="ui fluid action input">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
-			<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-		</div>
+<div class="ui small secondary filter menu tw-items-center tw-mx-0">
+	<form class="ui form ignore-dirty tw-flex-1">
+		{{if .PageIsExploreUsers}}
+			{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}}
+		{{else}}
+			{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.org_kind")}}
+		{{end}}
 	</form>
 	<!-- Sort -->
-	<div class="ui dropdown type jump item gt-mr-0">
+	<div class="ui small dropdown type jump item tw-mr-0">
 		<span class="text">
 			{{ctx.Locale.Tr "repo.issues.filter_sort"}}
 		</span>
 		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 		<div class="menu">
-			<a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-			<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+			<a class="{{if eq .SortType "newest"}}active {{end}}item" href="?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
+			<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+			<a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
+			<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
 		</div>
 	</div>
 </div>
diff --git a/templates/explore/user_list.tmpl b/templates/explore/user_list.tmpl
index 9abbff6d9c..f2cf939ffb 100644
--- a/templates/explore/user_list.tmpl
+++ b/templates/explore/user_list.tmpl
@@ -1,6 +1,6 @@
 <div class="flex-list">
 	{{range .Users}}
-		<div class="flex-item gt-ac">
+		<div class="flex-item tw-items-center">
 			<div class="flex-item-leading">
 				{{ctx.AvatarUtils.Avatar . 48}}
 			</div>
@@ -21,11 +21,13 @@
 							<a href="mailto:{{.Email}}">{{.Email}}</a>
 						</span>
 					{{end}}
-					<span class="flex-text-inline">{{svg "octicon-calendar"}}{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix) | Safe}}</span>
+					<span class="flex-text-inline">{{svg "octicon-calendar"}}{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix)}}</span>
 				</div>
 			</div>
 		</div>
 	{{else}}
-		<div class="flex-item">{{ctx.Locale.Tr "explore.user_no_results"}}</div>
+		<div class="flex-item">
+			{{ctx.Locale.Tr "search.no_results"}}
+		</div>
 	{{end}}
 </div>
diff --git a/templates/explore/users.tmpl b/templates/explore/users.tmpl
index 7e15ae3d47..e9046125c6 100644
--- a/templates/explore/users.tmpl
+++ b/templates/explore/users.tmpl
@@ -3,9 +3,7 @@
 	{{template "explore/navbar" .}}
 	<div class="ui container">
 		{{template "explore/search" .}}
-
 		{{template "explore/user_list" .}}
-
 		{{template "base/paginate" .}}
 	</div>
 </div>
diff --git a/templates/home.tmpl b/templates/home.tmpl
index 78364431e9..e6fd4ef020 100644
--- a/templates/home.tmpl
+++ b/templates/home.tmpl
@@ -1,6 +1,6 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home"}}{{end}}" class="page-content home">
-	<div class="gt-mb-5 gt-px-5">
+	<div class="tw-mb-8 tw-px-8">
 		<div class="center">
 			<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}">
 			<div class="hero">
@@ -17,7 +17,7 @@
 				{{svg "octicon-flame"}} {{ctx.Locale.Tr "startpage.install"}}
 			</h1>
 			<p class="large">
-				{{ctx.Locale.Tr "startpage.install_desc" | Str2html}}
+				{{ctx.Locale.Tr "startpage.install_desc"}}
 			</p>
 		</div>
 		<div class="eight wide center column">
@@ -25,7 +25,7 @@
 				{{svg "octicon-device-desktop"}} {{ctx.Locale.Tr "startpage.platform"}}
 			</h1>
 			<p class="large">
-				{{ctx.Locale.Tr "startpage.platform_desc" | Str2html}}
+				{{ctx.Locale.Tr "startpage.platform_desc"}}
 			</p>
 		</div>
 	</div>
@@ -35,7 +35,7 @@
 				{{svg "octicon-rocket"}} {{ctx.Locale.Tr "startpage.lightweight"}}
 			</h1>
 			<p class="large">
-				{{ctx.Locale.Tr "startpage.lightweight_desc" | Str2html}}
+				{{ctx.Locale.Tr "startpage.lightweight_desc"}}
 			</p>
 		</div>
 		<div class="eight wide center column">
@@ -43,7 +43,7 @@
 				{{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}}
 			</h1>
 			<p class="large">
-				{{ctx.Locale.Tr "startpage.license_desc" | Str2html}}
+				{{ctx.Locale.Tr "startpage.license_desc"}}
 			</p>
 		</div>
 	</div>
diff --git a/templates/install.tmpl b/templates/install.tmpl
index e9b267fa1c..8a6956b546 100644
--- a/templates/install.tmpl
+++ b/templates/install.tmpl
@@ -8,7 +8,7 @@
 			<div class="ui attached segment">
 				{{template "base/alert" .}}
 
-				<p>{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker" | Safe}}</p>
+				<p>{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker"}}</p>
 
 				<form class="ui form" action="{{AppSubUrl}}/" method="post">
 					<!-- Database Settings -->
@@ -28,7 +28,7 @@
 						</div>
 					</div>
 
-					<div class="gt-mt-4 gt-hidden" data-db-setting-for="common-host">
+					<div class="tw-mt-4 tw-hidden" data-db-setting-for="common-host">
 						<div class="inline required field {{if .Err_DbSetting}}error{{end}}">
 							<label for="db_host">{{ctx.Locale.Tr "install.host"}}</label>
 							<input id="db_host" name="db_host" value="{{.db_host}}">
@@ -47,7 +47,7 @@
 						</div>
 					</div>
 
-					<div class="gt-mt-4 gt-hidden" data-db-setting-for="postgres">
+					<div class="tw-mt-4 tw-hidden" data-db-setting-for="postgres">
 						<div class="inline required field">
 							<label>{{ctx.Locale.Tr "install.ssl_mode"}}</label>
 							<div class="ui selection database type dropdown">
@@ -68,11 +68,11 @@
 						</div>
 					</div>
 
-					<div class="gt-mt-4 gt-hidden" data-db-setting-for="sqlite3">
+					<div class="tw-mt-4 tw-hidden" data-db-setting-for="sqlite3">
 						<div class="inline required field {{if or .Err_DbPath .Err_DbSetting}}error{{end}}">
 							<label for="db_path">{{ctx.Locale.Tr "install.path"}}</label>
 							<input id="db_path" name="db_path" value="{{.db_path}}">
-							<span class="help">{{ctx.Locale.Tr "install.sqlite_helper" | Safe}}</span>
+							<span class="help">{{ctx.Locale.Tr "install.sqlite_helper"}}</span>
 						</div>
 					</div>
 
@@ -160,7 +160,7 @@
 
 					<!-- Email -->
 					<details class="optional field">
-						<summary class="right-content gt-py-3{{if .Err_SMTP}} text red{{end}}">
+						<summary class="right-content tw-py-2{{if .Err_SMTP}} text red{{end}}">
 							{{ctx.Locale.Tr "install.email_title"}}
 						</summary>
 						<div class="inline field">
@@ -200,7 +200,7 @@
 
 					<!-- Server and other services -->
 					<details class="optional field">
-						<summary class="right-content gt-py-3{{if .Err_Services}} text red{{end}}">
+						<summary class="right-content tw-py-2{{if .Err_Services}} text red{{end}}">
 							{{ctx.Locale.Tr "install.server_service_title"}}
 						</summary>
 						<div class="inline field">
@@ -298,7 +298,7 @@
 
 					<!-- Admin -->
 					<details class="optional field">
-						<summary class="right-content gt-py-3{{if .Err_Admin}} text red{{end}}">
+						<summary class="right-content tw-py-2{{if .Err_Admin}} text red{{end}}">
 							{{ctx.Locale.Tr "install.admin_title"}}
 						</summary>
 						<p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p>
@@ -327,7 +327,7 @@
 						<div class="right-content">
 							{{ctx.Locale.Tr "install.env_config_keys_prompt"}}
 						</div>
-						<div class="right-content gt-mt-3">
+						<div class="right-content tw-mt-2">
 							{{range .EnvConfigKeys}}<span class="ui label">{{.}}</span>{{end}}
 						</div>
 					</div>
@@ -338,7 +338,7 @@
 						<div class="right-content">
 							These configuration options will be written into: {{.CustomConfFile}}
 						</div>
-						<div class="right-content gt-mt-3">
+						<div class="right-content tw-mt-2">
 							<button class="ui primary button">{{ctx.Locale.Tr "install.install_btn_confirm"}}</button>
 						</div>
 					</div>
@@ -347,5 +347,5 @@
 		</div>
 	</div>
 </div>
-<img class="gt-hidden" src="{{AssetUrlPrefix}}/img/loading.png">
+<img class="tw-hidden" src="{{AssetUrlPrefix}}/img/loading.png">
 {{template "base/footer" .}}
diff --git a/templates/mail/auth/activate.tmpl b/templates/mail/auth/activate.tmpl
index a15afe3d49..b1bb4cb463 100644
--- a/templates/mail/auth/activate.tmpl
+++ b/templates/mail/auth/activate.tmpl
@@ -8,8 +8,8 @@
 
 {{$activate_url := printf "%suser/activate?code=%s" AppUrl (QueryEscape .Code)}}
 <body>
-	<p>{{.locale.Tr "mail.activate_account.text_1" (.DisplayName|DotEscape) AppName | Str2html}}</p><br>
-	<p>{{.locale.Tr "mail.activate_account.text_2" .ActiveCodeLives | Str2html}}</p><p><a href="{{$activate_url}}">{{$activate_url}}</a></p><br>
+	<p>{{.locale.Tr "mail.activate_account.text_1" (.DisplayName|DotEscape) AppName}}</p><br>
+	<p>{{.locale.Tr "mail.activate_account.text_2" .ActiveCodeLives}}</p><p><a href="{{$activate_url}}">{{$activate_url}}</a></p><br>
 	<p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
 
 	<p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p>
diff --git a/templates/mail/auth/activate_email.tmpl b/templates/mail/auth/activate_email.tmpl
index b15cc2a68a..3d32f80a4e 100644
--- a/templates/mail/auth/activate_email.tmpl
+++ b/templates/mail/auth/activate_email.tmpl
@@ -8,8 +8,8 @@
 
 {{$activate_url := printf "%suser/activate_email?code=%s&email=%s" AppUrl (QueryEscape .Code) (QueryEscape .Email)}}
 <body>
-	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | Str2html}}</p><br>
-	<p>{{.locale.Tr "mail.activate_email.text" .ActiveCodeLives | Str2html}}</p><p><a href="{{$activate_url}}">{{$activate_url}}</a></p><br>
+	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br>
+	<p>{{.locale.Tr "mail.activate_email.text" .ActiveCodeLives}}</p><p><a href="{{$activate_url}}">{{$activate_url}}</a></p><br>
 	<p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
 
 	<p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p>
diff --git a/templates/mail/auth/register_notify.tmpl b/templates/mail/auth/register_notify.tmpl
index 3cdb456fb3..62dbf7d927 100644
--- a/templates/mail/auth/register_notify.tmpl
+++ b/templates/mail/auth/register_notify.tmpl
@@ -8,10 +8,10 @@
 
 {{$set_pwd_url := printf "%[1]suser/forgot_password" AppUrl}}
 <body>
-	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | Str2html}}</p><br>
+	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br>
 	<p>{{.locale.Tr "mail.register_notify.text_1" AppName}}</p><br>
 	<p>{{.locale.Tr "mail.register_notify.text_2" .Username}}</p><p><a href="{{AppUrl}}user/login">{{AppUrl}}user/login</a></p><br>
-	<p>{{.locale.Tr "mail.register_notify.text_3" ($set_pwd_url | Escape) | Str2html}}</p><br>
+	<p>{{.locale.Tr "mail.register_notify.text_3" $set_pwd_url}}</p><br>
 
 	<p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p>
 </body>
diff --git a/templates/mail/auth/reset_passwd.tmpl b/templates/mail/auth/reset_passwd.tmpl
index 172844c954..55b1ecec3f 100644
--- a/templates/mail/auth/reset_passwd.tmpl
+++ b/templates/mail/auth/reset_passwd.tmpl
@@ -8,8 +8,8 @@
 
 {{$recover_url := printf "%suser/recover_account?code=%s" AppUrl (QueryEscape .Code)}}
 <body>
-	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | Str2html}}</p><br>
-	<p>{{.locale.Tr "mail.reset_password.text" .ResetPwdCodeLives | Str2html}}</p><p><a href="{{$recover_url}}">{{$recover_url}}</a></p><br>
+	<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br>
+	<p>{{.locale.Tr "mail.reset_password.text" .ResetPwdCodeLives}}</p><p><a href="{{$recover_url}}">{{$recover_url}}</a></p><br>
 	<p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
 
 	<p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p>
diff --git a/templates/mail/issue/assigned.tmpl b/templates/mail/issue/assigned.tmpl
index d02ea39918..5720319ee8 100644
--- a/templates/mail/issue/assigned.tmpl
+++ b/templates/mail/issue/assigned.tmpl
@@ -8,14 +8,14 @@
 	<title>{{.Subject}}</title>
 </head>
 
-{{$repo_url := printf "<a href='%s'>%s</a>" (Escape .Issue.Repo.HTMLURL) (Escape .Issue.Repo.FullName)}}
-{{$link := printf "<a href='%s'>#%d</a>" (Escape .Link) .Issue.Index}}
+{{$repo_url := HTMLFormat "<a href='%s'>%s</a>" .Issue.Repo.HTMLURL .Issue.Repo.FullName}}
+{{$link := HTMLFormat "<a href='%s'>#%d</a>" .Link .Issue.Index}}
 <body>
 	<p>
 		{{if .IsPull}}
-			{{.locale.Tr "mail.issue_assigned.pull" .Doer.Name $link $repo_url | Str2html}}
+			{{.locale.Tr "mail.issue_assigned.pull" .Doer.Name $link $repo_url}}
 		{{else}}
-			{{.locale.Tr "mail.issue_assigned.issue" .Doer.Name $link $repo_url | Str2html}}
+			{{.locale.Tr "mail.issue_assigned.issue" .Doer.Name $link $repo_url}}
 		{{end}}
 	</p>
 	<div class="footer">
diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl
index 422a4f0461..395b118d3e 100644
--- a/templates/mail/issue/default.tmpl
+++ b/templates/mail/issue/default.tmpl
@@ -16,56 +16,56 @@
 </head>
 
 <body>
-	{{if .IsMention}}<p>{{.locale.Tr "mail.issue.x_mentioned_you" .Doer.Name | Str2html}}</p>{{end}}
+	{{if .IsMention}}<p>{{.locale.Tr "mail.issue.x_mentioned_you" .Doer.Name}}</p>{{end}}
 	{{if eq .ActionName "push"}}
 		<p>
 			{{if .Comment.IsForcePush}}
 				{{$oldCommitUrl := printf "%s/commit/%s" .Comment.Issue.PullRequest.BaseRepo.HTMLURL .Comment.OldCommit}}
 				{{$oldShortSha := ShortSha .Comment.OldCommit}}
-				{{$oldCommitLink := printf "<a href='%[1]s'><b>%[2]s</b></a>" (Escape $oldCommitUrl) (Escape $oldShortSha)}}
+				{{$oldCommitLink := HTMLFormat "<a href='%[1]s'><b>%[2]s</b></a>" $oldCommitUrl $oldShortSha}}
 
 				{{$newCommitUrl := printf "%s/commit/%s" .Comment.Issue.PullRequest.BaseRepo.HTMLURL .Comment.NewCommit}}
 				{{$newShortSha := ShortSha .Comment.NewCommit}}
-				{{$newCommitLink := printf "<a href='%[1]s'><b>%[2]s</b></a>" (Escape $newCommitUrl) (Escape $newShortSha)}}
+				{{$newCommitLink := HTMLFormat "<a href='%[1]s'><b>%[2]s</b></a>" $newCommitUrl $newShortSha}}
 
-				{{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch $oldCommitLink $newCommitLink | Str2html}}
+				{{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch $oldCommitLink $newCommitLink}}
 			{{else}}
-				{{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits) | Str2html}}
+				{{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits)}}
 			{{end}}
 		</p>
 	{{end}}
 	<p>
 		{{if eq .ActionName "close"}}
-			{{.locale.Tr "mail.issue.action.close" (Escape .Doer.Name) .Issue.Index | Str2html}}
+			{{.locale.Tr "mail.issue.action.close" .Doer.Name .Issue.Index}}
 		{{else if eq .ActionName "reopen"}}
-			{{.locale.Tr "mail.issue.action.reopen" (Escape .Doer.Name) .Issue.Index | Str2html}}
+			{{.locale.Tr "mail.issue.action.reopen" .Doer.Name .Issue.Index}}
 		{{else if eq .ActionName "merge"}}
-			{{.locale.Tr "mail.issue.action.merge" (Escape .Doer.Name) .Issue.Index (Escape .Issue.PullRequest.BaseBranch) | Str2html}}
+			{{.locale.Tr "mail.issue.action.merge" .Doer.Name .Issue.Index .Issue.PullRequest.BaseBranch}}
 		{{else if eq .ActionName "approve"}}
-			{{.locale.Tr "mail.issue.action.approve" (Escape .Doer.Name) | Str2html}}
+			{{.locale.Tr "mail.issue.action.approve" .Doer.Name}}
 		{{else if eq .ActionName "reject"}}
-			{{.locale.Tr "mail.issue.action.reject" (Escape .Doer.Name) | Str2html}}
+			{{.locale.Tr "mail.issue.action.reject" .Doer.Name}}
 		{{else if eq .ActionName "review"}}
-			{{.locale.Tr "mail.issue.action.review" (Escape .Doer.Name) | Str2html}}
+			{{.locale.Tr "mail.issue.action.review" .Doer.Name}}
 		{{else if eq .ActionName "review_dismissed"}}
-			{{.locale.Tr "mail.issue.action.review_dismissed" (Escape .Doer.Name) (Escape .Comment.Review.Reviewer.Name) | Str2html}}
+			{{.locale.Tr "mail.issue.action.review_dismissed" .Doer.Name .Comment.Review.Reviewer.Name}}
 		{{else if eq .ActionName "ready_for_review"}}
-			{{.locale.Tr "mail.issue.action.ready_for_review" (Escape .Doer.Name) | Str2html}}
+			{{.locale.Tr "mail.issue.action.ready_for_review" .Doer.Name}}
 		{{end}}
 
 		{{- if eq .Body ""}}
 			{{if eq .ActionName "new"}}
-				{{.locale.Tr "mail.issue.action.new" (Escape .Doer.Name) .Issue.Index | Str2html}}
+				{{.locale.Tr "mail.issue.action.new" .Doer.Name .Issue.Index}}
 			{{end}}
 		{{else}}
-			{{.Body | Str2html}}
+			{{.Body}}
 		{{end -}}
 		{{- range .ReviewComments}}
 			<hr>
 			{{$.locale.Tr "mail.issue.in_tree_path" .TreePath}}
 			<div class="review">
 				<pre>{{.Patch}}</pre>
-				<div>{{.RenderedContent | Safe}}</div>
+				<div>{{.RenderedContent}}</div>
 			</div>
 		{{end -}}
 		{{if eq .ActionName "push"}}
diff --git a/templates/mail/notify/repo_transfer.tmpl b/templates/mail/notify/repo_transfer.tmpl
index 43d95b3ff0..8c8b276484 100644
--- a/templates/mail/notify/repo_transfer.tmpl
+++ b/templates/mail/notify/repo_transfer.tmpl
@@ -5,10 +5,10 @@
 	<title>{{.Subject}}</title>
 </head>
 
-{{$url := printf "<a href='%[1]s'>%[2]s</a>" (Escape .Link) (Escape .Repo)}}
+{{$url := HTMLFormat "<a href='%[1]s'>%[2]s</a>" .Link .Repo}}
 <body>
 	<p>{{.Subject}}.
-		{{.locale.Tr "mail.repo.transfer.body" $url | Str2html}}
+		{{.locale.Tr "mail.repo.transfer.body" $url}}
 	</p>
 	<p>
 		---
diff --git a/templates/mail/release.tmpl b/templates/mail/release.tmpl
index f588d8224f..90a3caa4c5 100644
--- a/templates/mail/release.tmpl
+++ b/templates/mail/release.tmpl
@@ -11,18 +11,18 @@
 
 </head>
 
-{{$release_url := printf "<a href='%s'>%s</a>" (.Release.HTMLURL | Escape) (.Release.TagName | Escape)}}
-{{$repo_url := printf "<a href='%s'>%s</a>" (.Release.Repo.HTMLURL | Escape) (.Release.Repo.FullName | Escape)}}
+{{$release_url := HTMLFormat "<a href='%s'>%s</a>" .Release.HTMLURL .Release.TagName}}
+{{$repo_url := HTMLFormat "<a href='%s'>%s</a>" .Release.Repo.HTMLURL .Release.Repo.FullName}}
 <body>
 	<p>
-		{{.locale.Tr "mail.release.new.text" .Release.Publisher.Name $release_url $repo_url | Str2html}}
+		{{.locale.Tr "mail.release.new.text" .Release.Publisher.Name $release_url $repo_url}}
 	</p>
 	<h4>{{.locale.Tr "mail.release.title" .Release.Title}}</h4>
 	<p>
 		{{.locale.Tr "mail.release.note"}}<br>
 		{{- if eq .Release.RenderedNote ""}}
 		{{else}}
-			{{.Release.RenderedNote | Str2html}}
+			{{.Release.RenderedNote}}
 		{{end -}}
 	</p>
 	<br><br>
diff --git a/templates/mail/team_invite.tmpl b/templates/mail/team_invite.tmpl
index d21b7843ec..cb0c0c0a50 100644
--- a/templates/mail/team_invite.tmpl
+++ b/templates/mail/team_invite.tmpl
@@ -5,7 +5,7 @@
 	<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
 </head>
 <body>
-	<p>{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName) | Str2html}}</p>
+	<p>{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName)}}</p>
 	<p>{{.locale.Tr "mail.team_invite.text_2"}}</p><p><a href="{{.InviteURL}}">{{.InviteURL}}</a></p>
 	<p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
 	<p>{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}</p>
diff --git a/templates/org/follow_unfollow.tmpl b/templates/org/follow_unfollow.tmpl
new file mode 100644
index 0000000000..ba0bd01efe
--- /dev/null
+++ b/templates/org/follow_unfollow.tmpl
@@ -0,0 +1,7 @@
+<button class="ui basic button tw-mr-0" hx-post="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}">
+	{{if $.IsFollowing}}
+		{{ctx.Locale.Tr "user.unfollow"}}
+	{{else}}
+		{{ctx.Locale.Tr "user.follow"}}
+	{{end}}
+</button>
diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl
index 7b912c1c56..7361df99ea 100644
--- a/templates/org/header.tmpl
+++ b/templates/org/header.tmpl
@@ -1,18 +1,32 @@
-{{with .Org}}
-	<div class="ui container">
-		<div class="ui vertically grid head">
-			<div class="column">
-				<div class="ui header gt-df gt-ac gt-word-break">
-					{{ctx.AvatarUtils.Avatar . 100}}
-					<span class="text thin grey"><a href="{{.HomeLink}}">{{.DisplayName}}</a></span>
-					<span class="org-visibility">
-						{{if .Visibility.IsLimited}}<div class="ui medium basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</div>{{end}}
-						{{if .Visibility.IsPrivate}}<div class="ui medium basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</div>{{end}}
-					</span>
-				</div>
-			</div>
+<div class="ui container tw-flex">
+	{{ctx.AvatarUtils.Avatar .Org 100 "org-avatar"}}
+	<div id="org-info" class="tw-flex tw-flex-col">
+		<div class="ui header">
+			{{.Org.DisplayName}}
+			<span class="org-visibility">
+				{{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}}
+				{{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}}
+			</span>
+			<span class="tw-flex tw-items-center tw-gap-1 tw-ml-auto tw-text-16 tw-whitespace-nowrap">
+				{{if .EnableFeed}}
+					<a class="ui basic label button tw-mr-0" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
+						{{svg "octicon-rss" 24}}
+					</a>
+				{{end}}
+				{{if .IsSigned}}
+					{{template "org/follow_unfollow" .}}
+				{{end}}
+			</span>
+		</div>
+		{{if .RenderedDescription}}<div class="render-content markup">{{.RenderedDescription}}</div>{{end}}
+		<div class="text light meta tw-mt-1">
+			{{if .Org.Location}}<div class="flex-text-block">{{svg "octicon-location"}} <span>{{.Org.Location}}</span></div>{{end}}
+			{{if .Org.Website}}<div class="flex-text-block">{{svg "octicon-link"}} <a class="muted" target="_blank" rel="noopener noreferrer me" href="{{.Org.Website}}">{{.Org.Website}}</a></div>{{end}}
+			{{if .IsSigned}}
+				{{if .Org.Email}}<div class="flex-text-block">{{svg "octicon-mail"}} <a class="muted" href="mailto:{{.Org.Email}}">{{.Org.Email}}</a></div>{{end}}
+			{{end}}
 		</div>
 	</div>
-{{end}}
+</div>
 
 {{template "org/menu" .}}
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index fc65d4691c..4851b69979 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -1,49 +1,14 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content organization profile">
-	<div class="ui container gt-df">
-		{{ctx.AvatarUtils.Avatar .Org 140 "org-avatar"}}
-		<div id="org-info">
-			<div class="ui header">
-				{{.Org.DisplayName}}
-				<span class="org-visibility">
-					{{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}}
-					{{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}}
-				</span>
-			</div>
-			{{if $.RenderedDescription}}<div class="render-content markup">{{$.RenderedDescription|Str2html}}</div>{{end}}
-			<div class="text grey meta">
-				{{if .Org.Location}}<div class="flex-text-block">{{svg "octicon-location"}} <span>{{.Org.Location}}</span></div>{{end}}
-				{{if .Org.Website}}<div class="flex-text-block">{{svg "octicon-link"}} <a target="_blank" rel="noopener noreferrer me" href="{{.Org.Website}}">{{.Org.Website}}</a></div>{{end}}
-				{{if $.IsSigned}}
-					{{if .Org.Email}}<div class="flex-text-block">{{svg "octicon-mail"}} <a class="muted" href="mailto:{{.Org.Email}}">{{.Org.Email}}</a></div>{{end}}
-				{{end}}
-			</div>
-		</div>
-		<div class="right menu">
-			{{if .EnableFeed}}
-			<a class="ui basic label button gt-mr-0" href="{{$.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
-				{{svg "octicon-rss" 24}}
-			</a>
-			{{end}}
-			<button class="link-action ui basic button gt-mr-0" data-url="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}">
-				{{if $.IsFollowing}}
-					{{ctx.Locale.Tr "user.unfollow"}}
-				{{else}}
-					{{ctx.Locale.Tr "user.follow"}}
-				{{end}}
-			</button>
-		</div>
-	</div>
-
-	{{template "org/menu" .}}
+	{{template "org/header" .}}
 
 	<div class="ui container">
 		<div class="ui mobile reversed stackable grid">
 			<div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column">
 				{{if .ProfileReadme}}
-					<div id="readme_profile" class="markup">{{.ProfileReadme | Str2html}}</div>
+					<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
 				{{end}}
-				{{template "explore/repo_search" .}}
+				{{template "shared/repo_search" .}}
 				{{template "explore/repo_list" .}}
 				{{template "base/paginate" .}}
 			</div>
@@ -51,7 +16,7 @@
 			{{if .ShowMemberAndTeamTab}}
 			<div class="ui five wide column">
 				{{if .CanCreateOrgRepo}}
-					<div class="center aligned">
+					<div class="center aligned tw-mb-4">
 						<a class="ui primary button" href="{{AppSubUrl}}/repo/create?org={{.Org.ID}}">{{ctx.Locale.Tr "new_repo"}}</a>
 						{{if not .DisableNewPullMirrors}}
 							<a class="ui primary button" href="{{AppSubUrl}}/repo/migrate?org={{.Org.ID}}&mirror=1">{{ctx.Locale.Tr "new_migrate"}}</a>
@@ -60,9 +25,9 @@
 					<div class="divider"></div>
 				{{end}}
 				{{if .NumMembers}}
-					<h4 class="ui top attached header gt-df">
-						<strong class="gt-f1">{{ctx.Locale.Tr "org.members"}}</strong>
-						<a class="text grey gt-df gt-ac" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a>
+					<h4 class="ui top attached header tw-flex">
+						<strong class="tw-flex-1">{{ctx.Locale.Tr "org.members"}}</strong>
+						<a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a>
 					</h4>
 					<div class="ui attached segment members">
 						{{$isMember := .IsOrganizationMember}}
@@ -74,9 +39,9 @@
 					</div>
 				{{end}}
 				{{if .IsOrganizationMember}}
-					<div class="ui top attached header gt-df">
-						<strong class="gt-f1">{{ctx.Locale.Tr "org.teams"}}</strong>
-						<a class="text grey gt-df gt-ac" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a>
+					<div class="ui top attached header tw-flex">
+						<strong class="tw-flex-1">{{ctx.Locale.Tr "org.teams"}}</strong>
+						<a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a>
 					</div>
 					<div class="ui attached table segment teams">
 						{{range .Teams}}
diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl
index e4ddb69805..4388dc9520 100644
--- a/templates/org/member/members.tmpl
+++ b/templates/org/member/members.tmpl
@@ -1,5 +1,5 @@
 {{template "base/head" .}}
-<div role="main" aria-label="{{.Title}}" class="page-content organization">
+<div role="main" aria-label="{{.Title}}" class="page-content organization members">
 	{{template "org/header" .}}
 	<div class="ui container">
 		{{template "base/alert" .}}
@@ -7,7 +7,7 @@
 		<div class="flex-list">
 			{{range .Members}}
 				{{$isPublic := index $.MembersIsPublicMember .ID}}
-				<div class="flex-item {{if $.PublicOnly}}gt-ac{{end}}">
+				<div class="flex-item {{if $.PublicOnly}}tw-items-center{{end}}">
 					<div class="flex-item-leading">
 						<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 48}}</a>
 					</div>
@@ -73,7 +73,7 @@
 		{{ctx.Locale.Tr "org.members.leave"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.members.leave.detail" `<span class="dataOrganizationName"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
@@ -82,7 +82,7 @@
 		{{ctx.Locale.Tr "org.members.remove"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.members.remove.detail" `<span class="name"></span>` `<span class="dataOrganizationName"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|SafeHTML) (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl
index 8a97711ce2..c519606d1f 100644
--- a/templates/org/menu.tmpl
+++ b/templates/org/menu.tmpl
@@ -1,50 +1,49 @@
 <div class="ui container">
-	<div class="ui secondary stackable pointing menu">
-		<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}">
-			{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
-			{{if .RepoCount}}
-				<div class="ui small label">{{.RepoCount}}</div>
+	<overflow-menu class="ui secondary pointing tabular borderless menu tw-mb-4">
+		<div class="overflow-menu-items">
+			<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}">
+				{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
+				{{if .RepoCount}}
+					<div class="ui small label">{{.RepoCount}}</div>
+				{{end}}
+			</a>
+			{{if .CanReadProjects}}
+			<a class="{{if .PageIsViewProjects}}active {{end}}item" href="{{$.Org.HomeLink}}/-/projects">
+				{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}}
+				{{if .ProjectCount}}
+					<div class="ui small label">{{.ProjectCount}}</div>
+				{{end}}
+			</a>
 			{{end}}
-		</a>
-		{{if .CanReadProjects}}
-		<a class="{{if .PageIsViewProjects}}active {{end}}item" href="{{$.Org.HomeLink}}/-/projects">
-			{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}}
-			{{if .ProjectCount}}
-				<div class="ui small label">{{.ProjectCount}}</div>
+			{{if and .IsPackageEnabled .CanReadPackages}}
+			<a class="{{if .IsPackagesPage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/packages">
+				{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
+			</a>
 			{{end}}
-		</a>
-		{{end}}
-		{{if and .IsPackageEnabled .CanReadPackages}}
-		<a class="item" href="{{$.Org.HomeLink}}/-/packages">
-			{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
-		</a>
-		{{end}}
-		{{if and .IsRepoIndexerEnabled .CanReadCode}}
-		<a class="item" href="{{$.Org.HomeLink}}/-/code">
-			{{svg "octicon-code"}}&nbsp;{{ctx.Locale.Tr "org.code"}}
-		</a>
-		{{end}}
-		{{if .NumMembers}}
+			{{if and .IsRepoIndexerEnabled .CanReadCode}}
+			<a class="{{if .IsCodePage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/code">
+				{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
+			</a>
+			{{end}}
+			{{if .NumMembers}}
 			<a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members">
-				{{svg "octicon-person"}}&nbsp;{{ctx.Locale.Tr "org.members"}}
+				{{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}}
 				<div class="ui small label">{{.NumMembers}}</div>
 			</a>
-		{{end}}
-		{{if .IsOrganizationMember}}
+			{{end}}
+			{{if .IsOrganizationMember}}
 			<a class="{{if $.PageIsOrgTeams}}active {{end}}item" href="{{$.OrgLink}}/teams">
-				{{svg "octicon-people"}}&nbsp;{{ctx.Locale.Tr "org.teams"}}
+				{{svg "octicon-people"}} {{ctx.Locale.Tr "org.teams"}}
 				{{if .NumTeams}}
 					<div class="ui small label">{{.NumTeams}}</div>
 				{{end}}
 			</a>
-		{{end}}
-
-		{{if .IsOrganizationOwner}}
-			<div class="right menu">
-				<a class="{{if .PageIsOrgSettings}}active {{end}}item" href="{{.OrgLink}}/settings">
-				{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
-				</a>
-			</div>
-		{{end}}
-	</div>
+			{{end}}
+			{{if .IsOrganizationOwner}}
+			<a class="{{if .PageIsOrgSettings}}active {{end}}item" href="{{.OrgLink}}/settings">
+			{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
+			</a>
+			{{end}}
+		</div>
+	</overflow-menu>
 </div>
diff --git a/templates/org/projects/list.tmpl b/templates/org/projects/list.tmpl
index 689091e5e0..80dde1c4d2 100644
--- a/templates/org/projects/list.tmpl
+++ b/templates/org/projects/list.tmpl
@@ -1,10 +1,9 @@
 {{template "base/head" .}}
 {{if .ContextUser.IsOrganization}}
-	<div role="main" aria-label="{{.Title}}" class="page-content repository packages">
-		{{template "shared/user/org_profile_avatar" .}}
+	<div role="main" aria-label="{{.Title}}" class="page-content organization projects">
+		{{template "org/header" .}}
 		<div class="ui container">
-		{{template "user/overview/header" .}}
-		{{template "projects/list" .}}
+			{{template "projects/list" .}}
 		</div>
 	</div>
 {{else}}
@@ -14,11 +13,9 @@
 				<div class="ui four wide column">
 					{{template "shared/user/profile_big_avatar" .}}
 				</div>
-				<div class="ui twelve wide column">
-				<div class="gt-mb-4">
+				<div class="ui twelve wide column tw-mb-4">
 					{{template "user/overview/header" .}}
-				</div>
-				{{template "projects/list" .}}
+					{{template "projects/list" .}}
 				</div>
 			</div>
 		</div>
diff --git a/templates/org/projects/view.tmpl b/templates/org/projects/view.tmpl
index 495204b06d..e1ab81c4cd 100644
--- a/templates/org/projects/view.tmpl
+++ b/templates/org/projects/view.tmpl
@@ -1,7 +1,7 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
 	{{template "shared/user/org_profile_avatar" .}}
-	<div class="ui container">
+	<div class="ui container tw-mb-4">
 		{{template "user/overview/header" .}}
 	</div>
 	<div class="ui container fluid padded">
diff --git a/templates/org/settings/blocked_users.tmpl b/templates/org/settings/blocked_users.tmpl
new file mode 100644
index 0000000000..eab5ec0007
--- /dev/null
+++ b/templates/org/settings/blocked_users.tmpl
@@ -0,0 +1,5 @@
+{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked_users")}}
+<div class="org-setting-content">
+	{{template "shared/user/blocked_users" .}}
+</div>
+{{template "org/settings/layout_footer" .}}
diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl
index 2cf8238f57..e1ef471e34 100644
--- a/templates/org/settings/delete.tmpl
+++ b/templates/org/settings/delete.tmpl
@@ -6,7 +6,7 @@
 				</h4>
 				<div class="ui attached error segment">
 					<div class="ui red message">
-						<p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt" | Str2html}}</p>
+						<p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt"}}</p>
 					</div>
 					<form class="ui form ignore-dirty" id="delete-form" action="{{.Link}}" method="post">
 						{{.CsrfTokenHtml}}
diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl
index b12ea8d9f4..25a562c975 100644
--- a/templates/org/settings/labels.tmpl
+++ b/templates/org/settings/labels.tmpl
@@ -1,8 +1,8 @@
 {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings labels")}}
 				<div class="org-setting-content">
-					<div class="gt-df gt-ac">
-						<div class="gt-f1">
-							{{ctx.Locale.Tr "org.settings.labels_desc" | Str2html}}
+					<div class="tw-flex tw-items-center">
+						<div class="tw-flex-1">
+							{{ctx.Locale.Tr "org.settings.labels_desc"}}
 						</div>
 						<button class="ui small primary new-label button">{{ctx.Locale.Tr "repo.issues.new_label"}}</button>
 					</div>
diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl
index 64ae20f0a3..ce792f667c 100644
--- a/templates/org/settings/navbar.tmpl
+++ b/templates/org/settings/navbar.tmpl
@@ -17,6 +17,9 @@
 			{{ctx.Locale.Tr "settings.applications"}}
 		</a>
 		{{end}}
+		<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users">
+			{{ctx.Locale.Tr "user.block.list"}}
+		</a>
 		{{if .EnablePackages}}
 		<a class="{{if .PageIsSettingsPackages}}active {{end}}item" href="{{.OrgLink}}/settings/packages">
 			{{ctx.Locale.Tr "packages.title"}}
diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl
index 31c0d85d89..62debfc0ae 100644
--- a/templates/org/settings/options.tmpl
+++ b/templates/org/settings/options.tmpl
@@ -8,7 +8,7 @@
 						{{.CsrfTokenHtml}}
 						<div class="required field {{if .Err_Name}}error{{end}}">
 							<label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}}
-								<span class="text red gt-hidden" id="org-name-change-prompt">
+								<span class="text red tw-hidden" id="org-name-change-prompt">
 									<br>{{ctx.Locale.Tr "org.settings.change_orgname_prompt"}}<br>{{ctx.Locale.Tr "org.settings.change_orgname_redirect_prompt"}}
 								</span>
 							</label>
diff --git a/templates/org/team/invite.tmpl b/templates/org/team/invite.tmpl
index e003d14757..1167828d14 100644
--- a/templates/org/team/invite.tmpl
+++ b/templates/org/team/invite.tmpl
@@ -7,7 +7,7 @@
 				{{ctx.AvatarUtils.Avatar .Organization 140}}
 			</div>
 			<div class="content">
-				<div class="header">{{ctx.Locale.Tr "org.teams.invite.title" .Team.Name .Organization.Name | Str2html}}</div>
+				<div class="header">{{ctx.Locale.Tr "org.teams.invite.title" .Team.Name .Organization.Name}}</div>
 				<div class="meta">{{ctx.Locale.Tr "org.teams.invite.by" .Inviter.Name}}</div>
 				<div class="description">{{ctx.Locale.Tr "org.teams.invite.description"}}</div>
 			</div>
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl
index da63d82967..5719328a27 100644
--- a/templates/org/team/members.tmpl
+++ b/templates/org/team/members.tmpl
@@ -9,12 +9,12 @@
 				{{template "org/team/navbar" .}}
 				{{if .IsOrganizationOwner}}
 					<div class="ui attached segment">
-						<form class="ui form ignore-dirty gt-df gt-fw gt-gap-3" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post">
+						<form class="ui form ignore-dirty tw-flex tw-flex-wrap tw-gap-2" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post">
 							{{.CsrfTokenHtml}}
 							<input type="hidden" name="uid" value="{{.SignedUser.ID}}">
-							<div id="search-user-box" class="ui search gt-mr-3"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{ctx.Locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}>
+							<div id="search-user-box" class="ui search tw-mr-2"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{ctx.Locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}>
 								<div class="ui input">
-									<input class="prompt" name="uname" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
+									<input class="prompt" name="uname" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" required>
 								</div>
 							</div>
 							<button class="ui primary button">{{ctx.Locale.Tr "org.teams.add_team_member"}}</button>
@@ -24,7 +24,7 @@
 				<div class="ui attached segment">
 					<div class="flex-list">
 						{{range .Team.Members}}
-							<div class="flex-item gt-ac">
+							<div class="flex-item tw-items-center">
 								<div class="flex-item-leading">
 									<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 32}}</a>
 								</div>
@@ -56,7 +56,7 @@
 				<div class="ui attached segment">
 					<div class="flex-list">
 						{{range .Invites}}
-							<div class="flex-item gt-ac">
+							<div class="flex-item tw-items-center">
 								<div class="flex-item-main">
 									{{.Email}}
 								</div>
@@ -81,7 +81,7 @@
 		{{ctx.Locale.Tr "org.members.remove"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.members.remove.detail" `<span class="name"></span>` `<span class="dataTeamName"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|SafeHTML) (`<span class="dataTeamName"></span>`|SafeHTML)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl
index 0178a20fbb..9608eac154 100644
--- a/templates/org/team/new.tmpl
+++ b/templates/org/team/new.tmpl
@@ -32,14 +32,14 @@
 									<div class="ui radio checkbox">
 										<input type="radio" name="repo_access" value="specific" {{if not .Team.IncludesAllRepositories}}checked{{end}}>
 										<label>{{ctx.Locale.Tr "org.teams.specific_repositories"}}</label>
-										<span class="help">{{ctx.Locale.Tr "org.teams.specific_repositories_helper" | Str2html}}</span>
+										<span class="help">{{ctx.Locale.Tr "org.teams.specific_repositories_helper"}}</span>
 									</div>
 								</div>
 								<div class="field">
 									<div class="ui radio checkbox">
 										<input type="radio" name="repo_access" value="all" {{if .Team.IncludesAllRepositories}}checked{{end}}>
 										<label>{{ctx.Locale.Tr "org.teams.all_repositories"}}</label>
-										<span class="help">{{ctx.Locale.Tr "org.teams.all_repositories_helper" | Str2html}}</span>
+										<span class="help">{{ctx.Locale.Tr "org.teams.all_repositories_helper"}}</span>
 									</div>
 								</div>
 
@@ -71,18 +71,18 @@
 							</div>
 							<div class="divider"></div>
 
-							<div class="team-units required grouped field {{if eq .Team.AccessMode 3}}gt-hidden{{end}}">
+							<div class="team-units required grouped field {{if eq .Team.AccessMode 3}}tw-hidden{{end}}">
 								<label>{{ctx.Locale.Tr "org.team_unit_desc"}}</label>
 								<table class="ui celled table">
 									<thead>
 										<tr>
 											<th>{{ctx.Locale.Tr "units.unit"}}</th>
 											<th class="center aligned">{{ctx.Locale.Tr "org.teams.none_access"}}
-											<span class="gt-vm" data-tooltip-content="{{ctx.Locale.Tr "org.teams.none_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th>
+											<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.none_access_helper"}}">{{svg "octicon-question" 16 "tw-ml-1"}}</span></th>
 											<th class="center aligned">{{ctx.Locale.Tr "org.teams.read_access"}}
-											<span class="gt-vm" data-tooltip-content="{{ctx.Locale.Tr "org.teams.read_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th>
+											<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.read_access_helper"}}">{{svg "octicon-question" 16 "tw-ml-1"}}</span></th>
 											<th class="center aligned">{{ctx.Locale.Tr "org.teams.write_access"}}
-											<span class="gt-vm" data-tooltip-content="{{ctx.Locale.Tr "org.teams.write_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th>
+											<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.write_access_helper"}}">{{svg "octicon-question" 16 "tw-ml-1"}}</span></th>
 										</tr>
 									</thead>
 									<tbody>
diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl
index 5a32eea64f..98b4854eb8 100644
--- a/templates/org/team/repositories.tmpl
+++ b/templates/org/team/repositories.tmpl
@@ -9,17 +9,17 @@
 				{{template "org/team/navbar" .}}
 				{{$canAddRemove := and $.IsOrganizationOwner (not $.Team.IncludesAllRepositories)}}
 				{{if $canAddRemove}}
-					<div class="ui attached segment gt-df gt-fw gt-gap-3">
-						<form class="ui form ignore-dirty gt-f1 gt-df" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/add" method="post">
+					<div class="ui attached segment tw-flex tw-flex-wrap tw-gap-2">
+						<form class="ui form ignore-dirty tw-flex-1 tw-flex" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/add" method="post">
 							{{.CsrfTokenHtml}}
 							<div id="search-repo-box" data-uid="{{.Org.ID}}" class="ui search">
 								<div class="ui input">
-									<input class="prompt" name="repo_name" placeholder="{{ctx.Locale.Tr "org.teams.search_repo_placeholder"}}" autocomplete="off" required>
+									<input class="prompt" name="repo_name" placeholder="{{ctx.Locale.Tr "search.repo_kind"}}" autocomplete="off" required>
 								</div>
 							</div>
-							<button class="ui primary button gt-ml-3">{{ctx.Locale.Tr "add"}}</button>
+							<button class="ui primary button tw-ml-2">{{ctx.Locale.Tr "add"}}</button>
 						</form>
-						<div class="gt-dib">
+						<div class="tw-inline-block">
 							<button class="ui primary button link-action" data-modal-confirm="{{ctx.Locale.Tr "org.teams.add_all_repos_desc"}}" data-url="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/addall">{{ctx.Locale.Tr "add_all"}}</button>
 							<button class="ui red button link-action" data-modal-confirm="{{ctx.Locale.Tr "org.teams.remove_all_repos_desc"}}" data-url="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/removeall">{{ctx.Locale.Tr "remove_all"}}</button>
 						</div>
@@ -28,7 +28,7 @@
 				<div class="ui attached segment">
 					<div class="flex-list">
 						{{range .Team.Repos}}
-							<div class="flex-item gt-ac">
+							<div class="flex-item tw-items-center">
 								<div class="flex-item-leading">
 									{{template "repo/icon" .}}
 								</div>
diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl
index 29e7cf7cdd..9311a46e38 100644
--- a/templates/org/team/sidebar.tmpl
+++ b/templates/org/team/sidebar.tmpl
@@ -27,16 +27,16 @@
 		</div>
 		{{if eq .Team.LowerName "owners"}}
 			<div class="item">
-				{{ctx.Locale.Tr "org.teams.owners_permission_desc" | Str2html}}
+				{{ctx.Locale.Tr "org.teams.owners_permission_desc"}}
 			</div>
 		{{else}}
 			<div class="item">
 				<h3>{{ctx.Locale.Tr "org.team_access_desc"}}</h3>
 				<ul>
 					{{if .Team.IncludesAllRepositories}}
-						<li>{{ctx.Locale.Tr "org.teams.all_repositories" | Str2html}}</li>
+						<li>{{ctx.Locale.Tr "org.teams.all_repositories"}}</li>
 					{{else}}
-						<li>{{ctx.Locale.Tr "org.teams.specific_repositories" | Str2html}}</li>
+						<li>{{ctx.Locale.Tr "org.teams.specific_repositories"}}</li>
 					{{end}}
 					{{if .Team.CanCreateOrgRepo}}
 						<li>{{ctx.Locale.Tr "org.teams.can_create_org_repo"}}</li>
@@ -44,10 +44,10 @@
 				</ul>
 				{{if (eq .Team.AccessMode 2)}}
 					<h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3>
-					{{ctx.Locale.Tr "org.teams.write_permission_desc" | Str2html}}
+					{{ctx.Locale.Tr "org.teams.write_permission_desc"}}
 				{{else if (eq .Team.AccessMode 3)}}
 					<h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3>
-					{{ctx.Locale.Tr "org.teams.admin_permission_desc" | Str2html}}
+					{{ctx.Locale.Tr "org.teams.admin_permission_desc"}}
 				{{else}}
 					<table class="ui table">
 						<thead>
@@ -88,7 +88,7 @@
 		{{ctx.Locale.Tr "org.teams.leave"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.teams.leave.detail" `<span class="name"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|SafeHTML)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl
index f4ceada2a7..53c909ee9c 100644
--- a/templates/org/team/teams.tmpl
+++ b/templates/org/team/teams.tmpl
@@ -49,7 +49,7 @@
 		{{ctx.Locale.Tr "org.teams.leave"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.teams.leave.detail" `<span class="name"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|SafeHTML)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/package/content/alpine.tmpl b/templates/package/content/alpine.tmpl
index a1003cd6ff..5c144b9779 100644
--- a/templates/package/content/alpine.tmpl
+++ b/templates/package/content/alpine.tmpl
@@ -3,13 +3,13 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.alpine.registry" | Safe}}</label>
-				<div class="markup"><pre class="code-block"><code><gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine"></gitea-origin-url>/$branch/$repository</code></pre></div>
-				<p>{{ctx.Locale.Tr "packages.alpine.registry.info" | Safe}}</p>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.alpine.registry"}}</label>
+				<div class="markup"><pre class="code-block"><code><origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine"></origin-url>/$branch/$repository</code></pre></div>
+				<p>{{ctx.Locale.Tr "packages.alpine.registry.info"}}</p>
 			</div>
 			<div class="field">
-				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.registry.key" | Safe}}</label>
-				<div class="markup"><pre class="code-block"><code>curl -JO <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key"></gitea-origin-url></code></pre></div>
+				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.registry.key"}}</label>
+				<div class="markup"><pre class="code-block"><code>curl -JO <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.install"}}</label>
@@ -18,7 +18,7 @@
 				</div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Alpine" "https://docs.gitea.com/usage/packages/alpine/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Alpine" "https://docs.gitea.com/usage/packages/alpine/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/cargo.tmpl b/templates/package/content/cargo.tmpl
index 4dd7c3f731..7fd88a284a 100644
--- a/templates/package/content/cargo.tmpl
+++ b/templates/package/content/cargo.tmpl
@@ -3,13 +3,13 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cargo.registry" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cargo.registry"}}</label>
 				<div class="markup"><pre class="code-block"><code>[registry]
 default = "gitea"
 
 [registries.gitea]
-index = "sparse+<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cargo/"></gitea-origin-url>" # Sparse index
-# index = "<gitea-origin-url data-url="{{AppSubUrl}}/{{.PackageDescriptor.Owner.Name}}/_cargo-index.git"></gitea-origin-url>" # Git
+index = "sparse+<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cargo/"></origin-url>" # Sparse index
+# index = "<origin-url data-url="{{AppSubUrl}}/{{.PackageDescriptor.Owner.Name}}/_cargo-index.git"></origin-url>" # Git
 
 [net]
 git-fetch-with-cli = true</code></pre></div>
@@ -19,7 +19,7 @@ git-fetch-with-cli = true</code></pre></div>
 				<div class="markup"><pre class="code-block"><code>cargo add {{.PackageDescriptor.Package.Name}}@{{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Cargo" "https://docs.gitea.com/usage/packages/cargo/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Cargo" "https://docs.gitea.com/usage/packages/cargo/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/chef.tmpl b/templates/package/content/chef.tmpl
index 0588c6e4b3..03ce9f852b 100644
--- a/templates/package/content/chef.tmpl
+++ b/templates/package/content/chef.tmpl
@@ -3,15 +3,15 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.chef.registry" | Safe}}</label>
-				<div class="markup"><pre class="code-block"><code>knife[:supermarket_site] = '<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/chef"></gitea-origin-url>'</code></pre></div>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.chef.registry"}}</label>
+				<div class="markup"><pre class="code-block"><code>knife[:supermarket_site] = '<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/chef"></origin-url>'</code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.chef.install"}}</label>
 				<div class="markup"><pre class="code-block"><code>knife supermarket install {{.PackageDescriptor.Package.Name}} {{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Chef" "https://docs.gitea.com/usage/packages/chef/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Chef" "https://docs.gitea.com/usage/packages/chef/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/composer.tmpl b/templates/package/content/composer.tmpl
index 862f1c6925..c2dc6345c3 100644
--- a/templates/package/content/composer.tmpl
+++ b/templates/package/content/composer.tmpl
@@ -3,11 +3,11 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.composer.registry" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.composer.registry"}}</label>
 				<div class="markup"><pre class="code-block"><code>{
 	"repositories": [{
 			"type": "composer",
-			"url": "<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/composer"></gitea-origin-url>"
+			"url": "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/composer"></origin-url>"
 		}
 	]
 }</code></pre></div>
@@ -17,7 +17,7 @@
 				<div class="markup"><pre class="code-block"><code>composer require {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Composer" "https://docs.gitea.com/usage/packages/composer/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Composer" "https://docs.gitea.com/usage/packages/composer/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/conan.tmpl b/templates/package/content/conan.tmpl
index 55b84d12b1..b68a45fde3 100644
--- a/templates/package/content/conan.tmpl
+++ b/templates/package/content/conan.tmpl
@@ -4,14 +4,14 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conan.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>conan remote add gitea <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conan"></gitea-origin-url></code></pre></div>
+				<div class="markup"><pre class="code-block"><code>conan remote add gitea <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conan"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conan.install"}}</label>
 				<div class="markup"><pre class="code-block"><code>conan install --remote=gitea {{.PackageDescriptor.Package.Name}}/{{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Conan" "https://docs.gitea.com/usage/packages/conan/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Conan" "https://docs.gitea.com/usage/packages/conan/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/conda.tmpl b/templates/package/content/conda.tmpl
index 0fd0c3db3f..031b51aa10 100644
--- a/templates/package/content/conda.tmpl
+++ b/templates/package/content/conda.tmpl
@@ -3,12 +3,12 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.conda.registry" | Safe}}</label>
-				<div class="markup"><pre class="code-block"><code>channel_alias: <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.conda.registry"}}</label>
+				<div class="markup"><pre class="code-block"><code>channel_alias: <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url>
 channels:
-&#32;&#32;- <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url>
+&#32;&#32;- <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url>
 default_channels:
-&#32;&#32;- <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url></code></pre></div>
+&#32;&#32;- <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conda.install"}}</label>
@@ -16,7 +16,7 @@ default_channels:
 				<div class="markup"><pre class="code-block"><code>conda install{{if $channel}} -c {{$channel}}{{end}} {{.PackageDescriptor.PackageProperties.GetByName "conda.name"}}={{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Conda" "https://docs.gitea.com/usage/packages/conda/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Conda" "https://docs.gitea.com/usage/packages/conda/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl
index f5ee902c94..fe393f4388 100644
--- a/templates/package/content/container.tmpl
+++ b/templates/package/content/container.tmpl
@@ -19,7 +19,7 @@
 				<div class="markup"><pre class="code-block"><code>{{range .PackageDescriptor.Files}}{{if eq .File.LowerName "manifest.json"}}{{.Properties.GetByName "container.digest"}}{{end}}{{end}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Container" "https://docs.gitea.com/usage/packages/container/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Container" "https://docs.gitea.com/usage/packages/container/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/cran.tmpl b/templates/package/content/cran.tmpl
index f9a3f70107..ae58e6f334 100644
--- a/templates/package/content/cran.tmpl
+++ b/templates/package/content/cran.tmpl
@@ -3,15 +3,15 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cran.registry" | Safe}}</label>
-				<div class="markup"><pre class="code-block"><code>options("repos" = c(getOption("repos"), c(gitea="<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cran"></gitea-origin-url>")))</code></pre></div>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cran.registry"}}</label>
+				<div class="markup"><pre class="code-block"><code>options("repos" = c(getOption("repos"), c(gitea="<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cran"></origin-url>")))</code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.cran.install"}}</label>
 				<div class="markup"><pre class="code-block"><code>install.packages("{{.PackageDescriptor.Package.Name}}")</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "CRAN" "https://docs.gitea.com/usage/packages/cran/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "CRAN" "https://docs.gitea.com/usage/packages/cran/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/debian.tmpl b/templates/package/content/debian.tmpl
index 1fde87f329..73b8257835 100644
--- a/templates/package/content/debian.tmpl
+++ b/templates/package/content/debian.tmpl
@@ -4,10 +4,10 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.debian.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>sudo curl <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key"></gitea-origin-url> -o /etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc
-echo "deb [signed-by=/etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc] <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian"></gitea-origin-url> $distribution $component" | sudo tee -a /etc/apt/sources.list.d/gitea.list
+				<div class="markup"><pre class="code-block"><code>sudo curl <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key"></origin-url> -o /etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc
+echo "deb [signed-by=/etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc] <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian"></origin-url> $distribution $component" | sudo tee -a /etc/apt/sources.list.d/gitea.list
 sudo apt update</code></pre></div>
-				<p>{{ctx.Locale.Tr "packages.debian.registry.info" | Safe}}</p>
+				<p>{{ctx.Locale.Tr "packages.debian.registry.info"}}</p>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.debian.install"}}</label>
@@ -16,7 +16,7 @@ sudo apt update</code></pre></div>
 				</div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Debian" "https://docs.gitea.com/usage/packages/debian/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Debian" "https://docs.gitea.com/usage/packages/debian/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/generic.tmpl b/templates/package/content/generic.tmpl
index 05aa4aecad..2fd952105f 100644
--- a/templates/package/content/generic.tmpl
+++ b/templates/package/content/generic.tmpl
@@ -6,12 +6,12 @@
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.generic.download"}}</label>
 				<div class="markup"><pre class="code-block"><code>
 {{- range .PackageDescriptor.Files -}}
-curl -OJ <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/generic/{{$.PackageDescriptor.Package.Name}}/{{$.PackageDescriptor.Version.Version}}/{{.File.Name}}"></gitea-origin-url>
+curl -OJ <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/generic/{{$.PackageDescriptor.Package.Name}}/{{$.PackageDescriptor.Version.Version}}/{{.File.Name}}"></origin-url>
 {{end -}}
 				</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Generic" "https://docs.gitea.com/usage/packages/generic" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Generic" "https://docs.gitea.com/usage/packages/generic"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/go.tmpl b/templates/package/content/go.tmpl
index f98fc69fb6..80d1ab231a 100644
--- a/templates/package/content/go.tmpl
+++ b/templates/package/content/go.tmpl
@@ -4,10 +4,10 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.go.install"}}</label>
-				<div class="markup"><pre class="code-block"><code>GOPROXY=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/go"></gitea-origin-url> go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}</code></pre></div>
+				<div class="markup"><pre class="code-block"><code>GOPROXY=<origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/go"></origin-url> go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Go" "https://docs.gitea.com/usage/packages/go" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Go" "https://docs.gitea.com/usage/packages/go"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/helm.tmpl b/templates/package/content/helm.tmpl
index 68e53133f1..da846e934d 100644
--- a/templates/package/content/helm.tmpl
+++ b/templates/package/content/helm.tmpl
@@ -4,7 +4,7 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.helm.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>helm repo add {{AppDomain}} <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/helm"></gitea-origin-url>
+				<div class="markup"><pre class="code-block"><code>helm repo add {{AppDomain}} <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/helm"></origin-url>
 helm repo update</code></pre></div>
 			</div>
 			<div class="field">
@@ -12,7 +12,7 @@ helm repo update</code></pre></div>
 				<div class="markup"><pre class="code-block"><code>helm install {{.PackageDescriptor.Package.Name}} {{AppDomain}}/{{.PackageDescriptor.Package.Name}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Helm" "https://docs.gitea.com/usage/packages/helm/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Helm" "https://docs.gitea.com/usage/packages/helm/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/maven.tmpl b/templates/package/content/maven.tmpl
index b2cd567e16..3a7de335de 100644
--- a/templates/package/content/maven.tmpl
+++ b/templates/package/content/maven.tmpl
@@ -3,28 +3,28 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.maven.registry" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.maven.registry"}}</label>
 				<div class="markup"><pre class="code-block"><code>&lt;repositories&gt;
 	&lt;repository&gt;
 		&lt;id&gt;gitea&lt;/id&gt;
-			&lt;url&gt;<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url>&lt;/url&gt;
+			&lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
 	&lt;/repository&gt;
 &lt;/repositories&gt;
 
 &lt;distributionManagement&gt;
 	&lt;repository&gt;
 		&lt;id&gt;gitea&lt;/id&gt;
-		&lt;url&gt;<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url>&lt;/url&gt;
+		&lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
 	&lt;/repository&gt;
 
 	&lt;snapshotRepository&gt;
 		&lt;id&gt;gitea&lt;/id&gt;
-		&lt;url&gt;<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url>&lt;/url&gt;
+		&lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
 	&lt;/snapshotRepository&gt;
 &lt;/distributionManagement&gt;</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.maven.install" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.maven.install"}}</label>
 				<div class="markup"><pre class="code-block"><code>&lt;dependency&gt;
 	&lt;groupId&gt;{{.PackageDescriptor.Metadata.GroupID}}&lt;/groupId&gt;
 	&lt;artifactId&gt;{{.PackageDescriptor.Metadata.ArtifactID}}&lt;/artifactId&gt;
@@ -37,10 +37,10 @@
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.maven.download"}}</label>
-				<div class="markup"><pre class="code-block"><code>mvn dependency:get -DremoteRepositories=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url> -Dartifact={{.PackageDescriptor.Metadata.GroupID}}:{{.PackageDescriptor.Metadata.ArtifactID}}:{{.PackageDescriptor.Version.Version}}</code></pre></div>
+				<div class="markup"><pre class="code-block"><code>mvn dependency:get -DremoteRepositories=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url> -Dartifact={{.PackageDescriptor.Metadata.GroupID}}:{{.PackageDescriptor.Metadata.ArtifactID}}:{{.PackageDescriptor.Version.Version}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Maven" "https://docs.gitea.com/usage/packages/maven/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Maven" "https://docs.gitea.com/usage/packages/maven/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/npm.tmpl b/templates/package/content/npm.tmpl
index 882e999bed..a78a07d874 100644
--- a/templates/package/content/npm.tmpl
+++ b/templates/package/content/npm.tmpl
@@ -3,8 +3,8 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.npm.registry" | Safe}}</label>
-				<div class="markup"><pre class="code-block"><code>{{if .PackageDescriptor.Metadata.Scope}}{{.PackageDescriptor.Metadata.Scope}}:{{end}}registry=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/npm/"></gitea-origin-url></code></pre></div>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.npm.registry"}}</label>
+				<div class="markup"><pre class="code-block"><code>{{if .PackageDescriptor.Metadata.Scope}}{{.PackageDescriptor.Metadata.Scope}}:{{end}}registry=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/npm/"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.npm.install"}}</label>
@@ -15,7 +15,7 @@
 				<div class="markup"><pre class="code-block"><code>&quot;{{.PackageDescriptor.Package.Name}}&quot;: &quot;{{.PackageDescriptor.Version.Version}}&quot;</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "npm" "https://docs.gitea.com/usage/packages/npm/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "npm" "https://docs.gitea.com/usage/packages/npm/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl
index 04dac89843..f1fe420c0b 100644
--- a/templates/package/content/nuget.tmpl
+++ b/templates/package/content/nuget.tmpl
@@ -4,24 +4,23 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.nuget.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>dotnet nuget add source --name {{.PackageDescriptor.Owner.Name}} --username your_username --password your_token <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/nuget/index.json"></gitea-origin-url></code></pre></div>
+				<div class="markup"><pre class="code-block"><code>dotnet nuget add source --name {{.PackageDescriptor.Owner.Name}} --username your_username --password your_token <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/nuget/index.json"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.nuget.install"}}</label>
 				<div class="markup"><pre class="code-block"><code>dotnet add package --source {{.PackageDescriptor.Owner.Name}} --version {{.PackageDescriptor.Version.Version}} {{.PackageDescriptor.Package.Name}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "NuGet" "https://docs.gitea.com/usage/packages/nuget/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "NuGet" "https://docs.gitea.com/usage/packages/nuget/"}}</label>
 			</div>
 		</div>
 	</div>
 
-	{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes}}
+	{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes .PackageDescriptor.Metadata.Readme}}
 		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4>
-		<div class="ui attached segment">
-			{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{end}}
-			{{if .PackageDescriptor.Metadata.ReleaseNotes}}{{Str2html .PackageDescriptor.Metadata.ReleaseNotes}}{{end}}
-		</div>
+		{{if .PackageDescriptor.Metadata.Description}}<div class="ui attached segment">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Description}}</div>{{end}}
+		{{if .PackageDescriptor.Metadata.Readme}}<div class="ui attached segment markup markdown">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Readme}}</div>{{end}}
+		{{if .PackageDescriptor.Metadata.ReleaseNotes}}<div class="ui attached segment">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.ReleaseNotes}}</div>{{end}}
 	{{end}}
 
 	{{if .PackageDescriptor.Metadata.Dependencies}}
diff --git a/templates/package/content/pub.tmpl b/templates/package/content/pub.tmpl
index 8657d55dbf..f2c7ac938f 100644
--- a/templates/package/content/pub.tmpl
+++ b/templates/package/content/pub.tmpl
@@ -4,10 +4,10 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.pub.install"}}</label>
-				<div class="markup"><pre class="code-block"><code>dart pub add {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}} --hosted-url=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pub/"></gitea-origin-url></code></pre></div>
+				<div class="markup"><pre class="code-block"><code>dart pub add {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}} --hosted-url=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pub/"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Pub" "https://docs.gitea.com/usage/packages/pub/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Pub" "https://docs.gitea.com/usage/packages/pub/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/pypi.tmpl b/templates/package/content/pypi.tmpl
index ef9beb4280..817fced97b 100644
--- a/templates/package/content/pypi.tmpl
+++ b/templates/package/content/pypi.tmpl
@@ -4,10 +4,10 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.pypi.install"}}</label>
-				<div class="markup"><pre class="code-block"><code>pip install --index-url <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></gitea-origin-url> {{.PackageDescriptor.Package.Name}}</code></pre></div>
+				<div class="markup"><pre class="code-block"><code>pip install --index-url <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></origin-url> {{.PackageDescriptor.Package.Name}}</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/rpm.tmpl b/templates/package/content/rpm.tmpl
index 0f128fd3fb..3faa8a0dc7 100644
--- a/templates/package/content/rpm.tmpl
+++ b/templates/package/content/rpm.tmpl
@@ -11,13 +11,13 @@
 # {{ctx.Locale.Tr "packages.rpm.distros.redhat"}}
 {{- range $group := .Groups}}
 	{{- if $group}}{{$group = print "/" $group}}{{end}}
-dnf config-manager --add-repo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></gitea-origin-url>
+dnf config-manager --add-repo <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></origin-url>
 {{- end}}
 
 # {{ctx.Locale.Tr "packages.rpm.distros.suse"}}
 {{- range $group := .Groups}}
 	{{- if $group}}{{$group = print "/" $group}}{{end}}
-zypper addrepo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></gitea-origin-url>
+zypper addrepo <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></origin-url>
 {{- end}}</code></pre></div>
 			</div>
 			<div class="field">
@@ -31,7 +31,7 @@ zypper install {{$.PackageDescriptor.Package.Name}}</code></pre>
 				</div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "RPM" "https://docs.gitea.com/usage/packages/rpm/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "RPM" "https://docs.gitea.com/usage/packages/rpm/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/rubygems.tmpl b/templates/package/content/rubygems.tmpl
index 180ff60f7d..610dfc7856 100644
--- a/templates/package/content/rubygems.tmpl
+++ b/templates/package/content/rubygems.tmpl
@@ -3,17 +3,17 @@
 	<div class="ui attached segment">
 		<div class="ui form">
 			<div class="field">
-				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rubygems.install" | Safe}}:</label>
-				<div class="markup"><pre class="code-block"><code>gem install {{.PackageDescriptor.Package.Name}} --version &quot;{{.PackageDescriptor.Version.Version}}&quot; --source &quot;<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></gitea-origin-url>&quot;</code></pre></div>
+				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rubygems.install"}}:</label>
+				<div class="markup"><pre class="code-block"><code>gem install {{.PackageDescriptor.Package.Name}} --version &quot;{{.PackageDescriptor.Version.Version}}&quot; --source &quot;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></origin-url>&quot;</code></pre></div>
 			</div>
 			<div class="field">
 				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.rubygems.install2"}}:</label>
-				<div class="markup"><pre class="code-block"><code>source "<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></gitea-origin-url>" do
+				<div class="markup"><pre class="code-block"><code>source "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></origin-url>" do
 	gem "{{.PackageDescriptor.Package.Name}}", "{{.PackageDescriptor.Version.Version}}"
 end</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "RubyGems" "https://docs.gitea.com/usage/packages/rubygems/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "RubyGems" "https://docs.gitea.com/usage/packages/rubygems/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/swift.tmpl b/templates/package/content/swift.tmpl
index ca36033df9..aacbc83980 100644
--- a/templates/package/content/swift.tmpl
+++ b/templates/package/content/swift.tmpl
@@ -4,10 +4,10 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.swift.registry"}}</label>
-				<div class="markup"><pre class="code-block"><code>swift package-registry set <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/swift"></gitea-origin-url></code></pre></div>
+				<div class="markup"><pre class="code-block"><code>swift package-registry set <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/swift"></origin-url></code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.swift.install" | Safe}}</label>
+				<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.swift.install"}}</label>
 				<div class="markup"><pre class="code-block"><code>dependencies: [
 	.package(id: "{{.PackageDescriptor.Package.Name}}", from:"{{.PackageDescriptor.Version.Version}}")
 ]</code></pre></div>
@@ -17,7 +17,7 @@
 				<div class="markup"><pre class="code-block"><code>swift package resolve</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Swift" "https://docs.gitea.com/usage/packages/swift/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Swift" "https://docs.gitea.com/usage/packages/swift/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/content/vagrant.tmpl b/templates/package/content/vagrant.tmpl
index bbb461e4fb..7666284b87 100644
--- a/templates/package/content/vagrant.tmpl
+++ b/templates/package/content/vagrant.tmpl
@@ -4,10 +4,10 @@
 		<div class="ui form">
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.vagrant.install"}}</label>
-				<div class="markup"><pre class="code-block"><code>vagrant box add --box-version {{.PackageDescriptor.Version.Version}} "<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"></gitea-origin-url>"</code></pre></div>
+				<div class="markup"><pre class="code-block"><code>vagrant box add --box-version {{.PackageDescriptor.Version.Version}} "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"></origin-url>"</code></pre></div>
 			</div>
 			<div class="field">
-				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Vagrant" "https://docs.gitea.com/usage/packages/vagrant/" | Safe}}</label>
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Vagrant" "https://docs.gitea.com/usage/packages/vagrant/"}}</label>
 			</div>
 		</div>
 	</div>
diff --git a/templates/package/metadata/alpine.tmpl b/templates/package/metadata/alpine.tmpl
index 73cbc06aac..3e7f10f66a 100644
--- a/templates/package/metadata/alpine.tmpl
+++ b/templates/package/metadata/alpine.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "alpine"}}
 	{{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}</div>{{end}}
 	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/cargo.tmpl b/templates/package/metadata/cargo.tmpl
index c8471a71ef..5ad3c20a93 100644
--- a/templates/package/metadata/cargo.tmpl
+++ b/templates/package/metadata/cargo.tmpl
@@ -1,7 +1,7 @@
 {{if eq .PackageDescriptor.Package.Type "cargo"}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/chef.tmpl b/templates/package/metadata/chef.tmpl
index fa6e068d23..23a9ce3ec0 100644
--- a/templates/package/metadata/chef.tmpl
+++ b/templates/package/metadata/chef.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "chef"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/composer.tmpl b/templates/package/metadata/composer.tmpl
index fbdc33f73d..0f6ff9d6f2 100644
--- a/templates/package/metadata/composer.tmpl
+++ b/templates/package/metadata/composer.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "composer"}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.Name}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.Homepage}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.Homepage}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{range .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Homepage}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.Homepage}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{range .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/conan.tmpl b/templates/package/metadata/conan.tmpl
index 40bda555bb..4e05ec2587 100644
--- a/templates/package/metadata/conan.tmpl
+++ b/templates/package/metadata/conan.tmpl
@@ -1,6 +1,6 @@
 {{if eq .PackageDescriptor.Package.Type "conan"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/conda.tmpl b/templates/package/metadata/conda.tmpl
index f70e2b2a1c..3628686e13 100644
--- a/templates/package/metadata/conda.tmpl
+++ b/templates/package/metadata/conda.tmpl
@@ -1,6 +1,6 @@
 {{if eq .PackageDescriptor.Package.Type "conda"}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/container.tmpl b/templates/package/metadata/container.tmpl
index b05ef0b846..f5abb7ef6e 100644
--- a/templates/package/metadata/container.tmpl
+++ b/templates/package/metadata/container.tmpl
@@ -1,9 +1,9 @@
 {{if eq .PackageDescriptor.Package.Type "container"}}
-	<div class="item" title="{{ctx.Locale.Tr "packages.container.details.type"}}">{{svg "octicon-package" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Type.Name}}</div>
-	{{if .PackageDescriptor.Metadata.Platform}}<div class="item" title="{{ctx.Locale.Tr "packages.container.details.platform"}}">{{svg "octicon-cpu" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Platform}}</div>{{end}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.Licenses}}<div class="item">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Licenses}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
+	<div class="item" title="{{ctx.Locale.Tr "packages.container.details.type"}}">{{svg "octicon-package" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Type.Name}}</div>
+	{{if .PackageDescriptor.Metadata.Platform}}<div class="item" title="{{ctx.Locale.Tr "packages.container.details.platform"}}">{{svg "octicon-cpu" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Platform}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Licenses}}<div class="item">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Licenses}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/helm.tmpl b/templates/package/metadata/helm.tmpl
index 499f77e80d..50ea484999 100644
--- a/templates/package/metadata/helm.tmpl
+++ b/templates/package/metadata/helm.tmpl
@@ -1,4 +1,4 @@
 {{if eq .PackageDescriptor.Package.Type "helm"}}
-	{{range .PackageDescriptor.Metadata.Maintainers}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.Name}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.Home}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.Home}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{range .PackageDescriptor.Metadata.Maintainers}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Home}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.Home}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/maven.tmpl b/templates/package/metadata/maven.tmpl
index 36f5eca840..548be61790 100644
--- a/templates/package/metadata/maven.tmpl
+++ b/templates/package/metadata/maven.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "maven"}}
-	{{if .PackageDescriptor.Metadata.Name}}<div class="item">{{svg "octicon-note" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Name}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Name}}<div class="item">{{svg "octicon-note" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Name}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/npm.tmpl b/templates/package/metadata/npm.tmpl
index 9794d851af..df37504e37 100644
--- a/templates/package/metadata/npm.tmpl
+++ b/templates/package/metadata/npm.tmpl
@@ -1,8 +1,8 @@
 {{if eq .PackageDescriptor.Package.Type "npm"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 	{{range .PackageDescriptor.VersionProperties}}
-		{{if eq .Name "npm.tag"}}<div class="item" title="{{ctx.Locale.Tr "packages.npm.details.tag"}}">{{svg "octicon-versions" 16 "gt-mr-3"}} {{.Value}}</div>{{end}}
+		{{if eq .Name "npm.tag"}}<div class="item" title="{{ctx.Locale.Tr "packages.npm.details.tag"}}">{{svg "octicon-versions" 16 "tw-mr-2"}} {{.Value}}</div>{{end}}
 	{{end}}
 {{end}}
diff --git a/templates/package/metadata/nuget.tmpl b/templates/package/metadata/nuget.tmpl
index f25e1c3b63..5534577bd2 100644
--- a/templates/package/metadata/nuget.tmpl
+++ b/templates/package/metadata/nuget.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "nuget"}}
-	{{if .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Authors}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Authors}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/pub.tmpl b/templates/package/metadata/pub.tmpl
index 1e4a90e78c..16f7cec370 100644
--- a/templates/package/metadata/pub.tmpl
+++ b/templates/package/metadata/pub.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "pub"}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/pypi.tmpl b/templates/package/metadata/pypi.tmpl
index f447cb7f4f..3d9b213907 100644
--- a/templates/package/metadata/pypi.tmpl
+++ b/templates/package/metadata/pypi.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "pypi"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/rpm.tmpl b/templates/package/metadata/rpm.tmpl
index 026f129590..eda8a489f3 100644
--- a/templates/package/metadata/rpm.tmpl
+++ b/templates/package/metadata/rpm.tmpl
@@ -1,4 +1,4 @@
 {{if eq .PackageDescriptor.Package.Type "rpm"}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/rubygems.tmpl b/templates/package/metadata/rubygems.tmpl
index 62150b1a43..9b11287691 100644
--- a/templates/package/metadata/rubygems.tmpl
+++ b/templates/package/metadata/rubygems.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "rubygems"}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>	{{end}}
-	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>	{{end}}
+	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/swift.tmpl b/templates/package/metadata/swift.tmpl
index 326ebe1a94..fdffb6dede 100644
--- a/templates/package/metadata/swift.tmpl
+++ b/templates/package/metadata/swift.tmpl
@@ -1,4 +1,4 @@
 {{if eq .PackageDescriptor.Package.Type "swift"}}
 	{{if .PackageDescriptor.Metadata.Author.String}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/vagrant.tmpl b/templates/package/metadata/vagrant.tmpl
index a92398a275..4628a2dcbb 100644
--- a/templates/package/metadata/vagrant.tmpl
+++ b/templates/package/metadata/vagrant.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "vagrant"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl
index 6ef62753e2..9424baf493 100644
--- a/templates/package/settings.tmpl
+++ b/templates/package/settings.tmpl
@@ -1,10 +1,16 @@
 {{template "base/head" .}}
-<div role="main" aria-label="{{.Title}}" class="page-content repository settings options">
-	{{template "shared/user/org_profile_avatar" .}}
+<div role="main" aria-label="{{.Title}}" class="page-content repository settings options{{if .ContextUser.IsOrganization}} organization{{end}}">
+	{{if .ContextUser.IsOrganization}}
+		{{template "org/header" .}}
+	{{else}}
+		{{template "shared/user/org_profile_avatar" .}}
+	{{end}}
 	<div class="ui container">
-		{{template "user/overview/header" .}}
+		{{if not .ContextUser.IsOrganization}}
+			{{template "user/overview/header" .}}
+		{{end}}
 		{{template "base/alert" .}}
-		<p><a href="{{.PackageDescriptor.FullWebLink}}">{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</a> / <strong>{{ctx.Locale.Tr "repo.settings"}}</strong></p>
+		<p><a href="{{.PackageDescriptor.VersionWebLink}}">{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</a> / <strong>{{ctx.Locale.Tr "repo.settings"}}</strong></p>
 		<h4 class="ui top attached header">
 			{{ctx.Locale.Tr "packages.settings.link"}}
 		</h4>
diff --git a/templates/package/shared/cargo.tmpl b/templates/package/shared/cargo.tmpl
index b452065881..7652231465 100644
--- a/templates/package/shared/cargo.tmpl
+++ b/templates/package/shared/cargo.tmpl
@@ -18,7 +18,7 @@
 			<button class="ui primary button">{{ctx.Locale.Tr "packages.owner.settings.cargo.rebuild"}}</button>
 		</form>
 		<div class="field">
-			<label>{{ctx.Locale.Tr "packages.registry.documentation" "Cargo" "https://docs.gitea.com/usage/packages/cargo/" | Safe}}</label>
+			<label>{{ctx.Locale.Tr "packages.registry.documentation" "Cargo" "https://docs.gitea.com/usage/packages/cargo/"}}</label>
 		</div>
 	</div>
 </div>
diff --git a/templates/package/shared/cleanup_rules/edit.tmpl b/templates/package/shared/cleanup_rules/edit.tmpl
index 8729494412..138a90791c 100644
--- a/templates/package/shared/cleanup_rules/edit.tmpl
+++ b/templates/package/shared/cleanup_rules/edit.tmpl
@@ -40,7 +40,7 @@
 		<div class="field {{if .Err_KeepPattern}}error{{end}}">
 			<label>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.keep.pattern"}}:</label>
 			<input name="keep_pattern" type="text" value="{{.CleanupRule.KeepPattern}}">
-			<p>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.keep.pattern.container" | Safe}}</p>
+			<p>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.keep.pattern.container"}}</p>
 		</div>
 		<div class="divider"></div>
 		<p>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.remove.title"}}</p>
diff --git a/templates/package/shared/cleanup_rules/preview.tmpl b/templates/package/shared/cleanup_rules/preview.tmpl
index 7a50d5ccca..cff8e8249f 100644
--- a/templates/package/shared/cleanup_rules/preview.tmpl
+++ b/templates/package/shared/cleanup_rules/preview.tmpl
@@ -19,7 +19,7 @@
 				<tr>
 					<td>{{.Package.Type.Name}}</td>
 					<td>{{.Package.Name}}</td>
-					<td><a href="{{.FullWebLink}}">{{.Version.Version}}</a></td>
+					<td><a href="{{.VersionWebLink}}">{{.Version.Version}}</a></td>
 					<td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td>
 					<td>{{FileSize .CalculateBlobSize}}</td>
 					<td>{{DateTime "short" .Version.CreatedUnix}}</td>
diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl
index 740a96bb8d..e4e8eca91e 100644
--- a/templates/package/shared/list.tmpl
+++ b/templates/package/shared/list.tmpl
@@ -1,16 +1,16 @@
 {{template "base/alert" .}}
 {{if .HasPackages}}
 <form class="ui form ignore-dirty">
-	<div class="ui fluid action input">
-		{{template "shared/searchinput" dict "Value" .Query}}
-		<select class="ui dropdown" name="type">
+	<div class="ui small fluid action input">
+		{{template "shared/search/input" dict "Value" .Query "Placeholder" (ctx.Locale.Tr "search.package_kind")}}
+		<select class="ui small dropdown" name="type">
 			<option value="">{{ctx.Locale.Tr "packages.filter.type"}}</option>
 			<option value="all">{{ctx.Locale.Tr "packages.filter.type.all"}}</option>
 			{{range $type := .AvailableTypes}}
 			<option{{if eq $.PackageType $type}} selected="selected"{{end}} value="{{$type}}">{{$type.Name}}</option>
 			{{end}}
 		</select>
-		<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
+		{{template "shared/search/button"}}
 	</div>
 </form>
 {{end}}
@@ -20,7 +20,7 @@
 		<div class="flex-item">
 			<div class="flex-item-main">
 				<div class="flex-item-title">
-					<a href="{{.FullWebLink}}">{{.Package.Name}}</a>
+					<a href="{{.VersionWebLink}}">{{.Package.Name}}</a>
 					<span class="ui label">{{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}}</span>
 				</div>
 				<div class="flex-item-body">
@@ -30,9 +30,9 @@
 						{{$hasRepositoryAccess = index $.RepositoryAccessMap .Repository.ID}}
 					{{end}}
 					{{if $hasRepositoryAccess}}
-						{{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) .Repository.Link (.Repository.FullName | Escape) | Safe}}
+						{{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink .Creator.GetDisplayName .Repository.Link .Repository.FullName}}
 					{{else}}
-						{{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}}
+						{{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink .Creator.GetDisplayName}}
 					{{end}}
 				</div>
 			</div>
@@ -45,12 +45,12 @@
 				<h2>{{ctx.Locale.Tr "packages.empty"}}</h2>
 				{{if and .Repository .CanWritePackages}}
 					{{$packagesUrl := URLJoin .Owner.HomeLink "-" "packages"}}
-					<p>{{ctx.Locale.Tr "packages.empty.repo" $packagesUrl | Safe}}</p>
+					<p>{{ctx.Locale.Tr "packages.empty.repo" $packagesUrl}}</p>
 				{{end}}
-				<p>{{ctx.Locale.Tr "packages.empty.documentation" "https://docs.gitea.com/usage/packages/overview/" | Safe}}</p>
+				<p>{{ctx.Locale.Tr "packages.empty.documentation" "https://docs.gitea.com/usage/packages/overview/"}}</p>
 			</div>
 		{{else}}
-			<p class="gt-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p>
+			<p class="tw-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p>
 		{{end}}
 	{{end}}
 	{{template "base/paginate" .}}
diff --git a/templates/package/shared/versionlist.tmpl b/templates/package/shared/versionlist.tmpl
index fcf3030fe6..e5c568e059 100644
--- a/templates/package/shared/versionlist.tmpl
+++ b/templates/package/shared/versionlist.tmpl
@@ -1,21 +1,21 @@
 <p><a href="{{.PackageDescriptor.PackageWebLink}}">{{.PackageDescriptor.Package.Name}}</a> / <strong>{{ctx.Locale.Tr "packages.versions"}}</strong></p>
 <form class="ui form ignore-dirty">
-	<div class="ui fluid action input">
-		{{template "shared/searchinput" dict "Value" .Query}}
-		<select class="ui dropdown" name="sort">
+	<div class="ui small fluid action input">
+		{{template "shared/search/input" dict "Value" .Query "Placeholder" (ctx.Locale.Tr "search.package_kind")}}
+		<select class="ui small dropdown" name="sort">
 			<option value="version_asc"{{if eq .Sort "version_asc"}} selected="selected"{{end}}>{{ctx.Locale.Tr "filter.string.asc"}}</option>
 			<option value="version_desc"{{if eq .Sort "version_desc"}} selected="selected"{{end}}>{{ctx.Locale.Tr "filter.string.desc"}}</option>
 			<option value="created_asc"{{if eq .Sort "created_asc"}} selected="selected"{{end}}>{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</option>
 			<option value="created_desc"{{if or (eq .Sort "") (eq .Sort "created_desc")}} selected="selected"{{end}}>{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</option>
 		</select>
 		{{if eq .PackageDescriptor.Package.Type "container"}}
-		<select class="ui dropdown" name="tagged">
+		<select class="ui small dropdown" name="tagged">
 			{{$isTagged := or (eq .Tagged "") (eq .Tagged "tagged")}}
 			<option value="tagged"{{if $isTagged}} selected="selected"{{end}}>{{ctx.Locale.Tr "packages.filter.container.tagged"}}</option>
 			<option value="untagged"{{if not $isTagged}} selected="selected"{{end}}>{{ctx.Locale.Tr "packages.filter.container.untagged"}}</option>
 		</select>
 		{{end}}
-		<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
+		{{template "shared/search/button"}}
 	</div>
 </form>
 <div>
@@ -23,15 +23,15 @@
 	<div class="flex-list">
 		<div class="flex-item">
 			<div class="flex-item-main">
-				<a class="flex-item-title" href="{{.FullWebLink}}">{{.Version.LowerVersion}}</a>
+				<a class="flex-item-title" href="{{.VersionWebLink}}">{{.Version.LowerVersion}}</a>
 				<div class="flex-item-body">
-					{{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}}
+					{{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink .Creator.GetDisplayName}}
 				</div>
 			</div>
 		</div>
 	</div>
 	{{else}}
-		<p class="gt-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p>
+		<p class="tw-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p>
 	{{end}}
 	{{template "base/paginate" .}}
 </div>
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl
index 553a46cfad..6beb249a7f 100644
--- a/templates/package/view.tmpl
+++ b/templates/package/view.tmpl
@@ -10,9 +10,9 @@
 			<div>
 				{{$timeStr := TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}
 				{{if .HasRepositoryAccess}}
-					{{ctx.Locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) .PackageDescriptor.Repository.Link (.PackageDescriptor.Repository.FullName | Escape) | Safe}}
+					{{ctx.Locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink .PackageDescriptor.Creator.GetDisplayName .PackageDescriptor.Repository.Link .PackageDescriptor.Repository.FullName}}
 				{{else}}
-					{{ctx.Locale.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) | Safe}}
+					{{ctx.Locale.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink .PackageDescriptor.Creator.GetDisplayName}}
 				{{end}}
 			</div>
 		</div>
@@ -43,12 +43,12 @@
 			<div class="issue-content-right ui segment">
 				<strong>{{ctx.Locale.Tr "packages.details"}}</strong>
 				<div class="ui relaxed list">
-					<div class="item">{{svg .PackageDescriptor.Package.Type.SVGName 16 "gt-mr-3"}} {{.PackageDescriptor.Package.Type.Name}}</div>
+					<div class="item">{{svg .PackageDescriptor.Package.Type.SVGName 16 "tw-mr-2"}} {{.PackageDescriptor.Package.Type.Name}}</div>
 					{{if .HasRepositoryAccess}}
-					<div class="item">{{svg "octicon-repo" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Repository.Link}}">{{.PackageDescriptor.Repository.FullName}}</a></div>
+					<div class="item">{{svg "octicon-repo" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Repository.Link}}">{{.PackageDescriptor.Repository.FullName}}</a></div>
 					{{end}}
-					<div class="item">{{svg "octicon-calendar" 16 "gt-mr-3"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}</div>
-					<div class="item">{{svg "octicon-download" 16 "gt-mr-3"}} {{.PackageDescriptor.Version.DownloadCount}}</div>
+					<div class="item">{{svg "octicon-calendar" 16 "tw-mr-2"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}</div>
+					<div class="item">{{svg "octicon-download" 16 "tw-mr-2"}} {{.PackageDescriptor.Version.DownloadCount}}</div>
 					{{template "package/metadata/alpine" .}}
 					{{template "package/metadata/cargo" .}}
 					{{template "package/metadata/chef" .}}
@@ -70,7 +70,7 @@
 					{{template "package/metadata/swift" .}}
 					{{template "package/metadata/vagrant" .}}
 					{{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}}
-					<div class="item">{{svg "octicon-database" 16 "gt-mr-3"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div>
+					<div class="item">{{svg "octicon-database" 16 "tw-mr-2"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div>
 					{{end}}
 				</div>
 				{{if not (eq .PackageDescriptor.Package.Type "container")}}
@@ -87,11 +87,11 @@
 				{{end}}
 				<div class="divider"></div>
 				<strong>{{ctx.Locale.Tr "packages.versions"}} ({{.TotalVersionCount}})</strong>
-				<a class="gt-float-right" href="{{$.PackageDescriptor.PackageWebLink}}/versions">{{ctx.Locale.Tr "packages.versions.view_all"}}</a>
+				<a class="tw-float-right" href="{{$.PackageDescriptor.PackageWebLink}}/versions">{{ctx.Locale.Tr "packages.versions.view_all"}}</a>
 				<div class="ui relaxed list">
 				{{range .LatestVersions}}
-					<div class="item gt-df">
-						<a class="gt-f1 gt-ellipsis" title="{{.Version}}" href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .LowerVersion}}">{{.Version}}</a>
+					<div class="item tw-flex">
+						<a class="tw-flex-1 gt-ellipsis" title="{{.Version}}" href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .LowerVersion}}">{{.Version}}</a>
 						<span class="text small">{{DateTime "short" .CreatedUnix}}</span>
 					</div>
 				{{end}}
@@ -100,10 +100,10 @@
 					<div class="divider"></div>
 					<div class="ui relaxed list">
 						{{if .HasRepositoryAccess}}
-						<div class="item">{{svg "octicon-issue-opened" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div>
+						<div class="item">{{svg "octicon-issue-opened" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div>
 						{{end}}
 						{{if .CanWritePackages}}
-						<div class="item">{{svg "octicon-tools" 16 "gt-mr-3"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div>
+						<div class="item">{{svg "octicon-tools" 16 "tw-mr-2"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div>
 						{{end}}
 					</div>
 				{{end}}
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index cbff82dd70..ec02e9a6fc 100644
--- a/templates/projects/list.tmpl
+++ b/templates/projects/list.tmpl
@@ -1,16 +1,16 @@
 {{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
-	<div class="gt-df gt-sb gt-mb-4">
+	<div class="tw-flex tw-justify-between tw-mb-4">
 		<div class="small-menu-items ui compact tiny menu list-header-toggle">
-			<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{$.Link}}?state=open&q={{$.Keyword}}">
-				{{svg "octicon-project-symlink" 16 "gt-mr-3"}}
+			<a class="item{{if not .IsShowClosed}} active{{end}}" href="?state=open&q={{$.Keyword}}">
+				{{svg "octicon-project-symlink" 16 "tw-mr-2"}}
 				{{ctx.Locale.PrettyNumber .OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 			</a>
-			<a class="item{{if .IsShowClosed}} active{{end}}" href="{{$.Link}}?state=closed&q={{$.Keyword}}">
-				{{svg "octicon-check" 16 "gt-mr-3"}}
+			<a class="item{{if .IsShowClosed}} active{{end}}" href="?state=closed&q={{$.Keyword}}">
+				{{svg "octicon-check" 16 "tw-mr-2"}}
 				{{ctx.Locale.PrettyNumber .ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 			</a>
 		</div>
-		<div class="gt-text-right">
+		<div class="tw-text-right">
 			<a class="ui small primary button" href="{{$.Link}}/new">{{ctx.Locale.Tr "repo.projects.new"}}</a>
 		</div>
 	</div>
@@ -21,13 +21,8 @@
 <div class="list-header">
 	<!-- Search -->
 	<form class="list-header-search ui form ignore-dirty">
-		<div class="ui small search fluid action input">
-			<input type="hidden" name="state" value="{{$.State}}">
-			{{template "shared/searchinput" dict "Value" .Keyword}}
-			<button class="ui small icon button" type="submit" aria-label="{{ctx.Locale.Tr "explore.search"}}">
-				{{svg "octicon-search"}}
-			</button>
-		</div>
+		<input type="hidden" name="state" value="{{$.State}}">
+		{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.project_kind")}}
 	</form>
 	<!-- Sort -->
 	<div class="list-header-sort ui small dropdown type jump item">
@@ -36,9 +31,9 @@
 		</span>
 		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 		<div class="menu">
-			<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&sort=oldest&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&sort=recentupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&sort=leastupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+			<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?q={{$.Keyword}}&sort=oldest&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+			<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=recentupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+			<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=leastupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
 		</div>
 	</div>
 </div>
@@ -46,7 +41,7 @@
 <div class="milestone-list">
 	{{range .Projects}}
 		<li class="milestone-card">
-			<h3 class="flex-text-block gt-m-0">
+			<h3 class="flex-text-block tw-m-0">
 				{{svg .IconName 16}}
 				<a class="muted" href="{{.Link ctx}}">{{.Title}}</a>
 			</h3>
@@ -75,7 +70,7 @@
 			</div>
 			{{if .Description}}
 			<div class="content">
-				{{.RenderedContent|Str2html}}
+				{{.RenderedContent}}
 			</div>
 			{{end}}
 		</li>
diff --git a/templates/projects/new.tmpl b/templates/projects/new.tmpl
index 711dbe842a..92ee36c1c4 100644
--- a/templates/projects/new.tmpl
+++ b/templates/projects/new.tmpl
@@ -55,7 +55,7 @@
 		</div>
 	</div>
 	<div class="divider"></div>
-	<div class="gt-text-right">
+	<div class="tw-text-right">
 		<a class="ui cancel button" href="{{$.CancelLink}}">
 			{{ctx.Locale.Tr "repo.milestones.cancel"}}
 		</a>
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index b3ad03c354..f9b85360e0 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -1,8 +1,8 @@
 {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
 
 <div class="ui container">
-	<div class="gt-df gt-sb gt-ac gt-mb-4">
-		<h2 class="gt-mb-0">{{.Project.Title}}</h2>
+	<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
+		<h2 class="tw-mb-0">{{.Project.Title}}</h2>
 		{{if $canWriteProject}}
 			<div class="ui compact mini menu">
 				<a class="item" href="{{.Link}}/edit?redirect=project">
@@ -41,9 +41,9 @@
 						</div>
 
 						<div class="field color-field">
-							<label for="new_project_column_color">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
-							<div class="color picker column">
-								<input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_project_column_color_picker" name="color">
+							<label for="new_project_column_color_picker">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
+							<div class="js-color-picker-input column">
+								<input maxlength="7" placeholder="#c320f6" id="new_project_column_color_picker" name="color">
 								{{template "repo/issue/label_precolors"}}
 							</div>
 						</div>
@@ -58,7 +58,7 @@
 		{{end}}
 	</div>
 
-	<div class="content">{{$.Project.RenderedContent|Str2html}}</div>
+	<div class="content">{{$.Project.RenderedContent}}</div>
 
 	<div class="divider"></div>
 </div>
@@ -66,17 +66,17 @@
 <div id="project-board">
 	<div class="board {{if .CanWriteProjects}}sortable{{end}}">
 		{{range .Columns}}
-			<div class="ui segment project-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
-				<div class="project-column-header">
-					<div class="ui large label project-column-title gt-py-2">
+			<div class="ui segment project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
+				<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
+					<div class="ui large label project-column-title tw-py-1">
 						<div class="ui small circular grey label project-column-issue-count">
 							{{.NumIssues ctx}}
 						</div>
-						{{.Title}}
+						<span class="project-column-title-label">{{.Title}}</span>
 					</div>
-					{{if and $canWriteProject (ne .ID 0)}}
+					{{if $canWriteProject}}
 						<div class="ui dropdown jump item">
-							<div class="gt-px-3">
+							<div class="tw-px-2">
 								{{svg "octicon-kebab-horizontal"}}
 							</div>
 							<div class="menu user-menu">
@@ -86,29 +86,20 @@
 								</a>
 								{{if not .Default}}
 									<a class="item show-modal button default-project-column-show"
-									data-modal="#default-project-column-modal-{{.ID}}"
-									data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}"
-									data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}"
-									data-url="{{$.Link}}/{{.ID}}/default">
+										data-modal="#default-project-column-modal-{{.ID}}"
+										data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}"
+										data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}"
+										data-url="{{$.Link}}/{{.ID}}/default">
 										{{svg "octicon-pin"}}
 										{{ctx.Locale.Tr "repo.projects.column.set_default"}}
 									</a>
-								{{else}}
-									<a class="item show-modal button default-project-column-show"
-									data-modal="#default-project-column-modal-{{.ID}}"
-									data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.unset_default"}}"
-									data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.unset_default_desc"}}"
-									data-url="{{$.Link}}/{{.ID}}/unsetdefault">
-										{{svg "octicon-pin-slash"}}
-										{{ctx.Locale.Tr "repo.projects.column.unset_default"}}
+									<a class="item show-modal button show-delete-project-column-modal"
+										data-modal="#delete-project-column-modal-{{.ID}}"
+										data-url="{{$.Link}}/{{.ID}}">
+										{{svg "octicon-trash"}}
+										{{ctx.Locale.Tr "repo.projects.column.delete"}}
 									</a>
 								{{end}}
-								<a class="item show-modal button show-delete-project-column-modal"
-									data-modal="#delete-project-column-modal-{{.ID}}"
-									data-url="{{$.Link}}/{{.ID}}">
-									{{svg "octicon-trash"}}
-									{{ctx.Locale.Tr "repo.projects.column.delete"}}
-								</a>
 
 								<div class="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}">
 									<div class="header">
@@ -123,8 +114,8 @@
 
 											<div class="field color-field">
 												<label for="new_project_column_color">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
-												<div class="color picker column">
-													<input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}">
+												<div class="js-color-picker-input column">
+													<input maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}">
 													{{template "repo/issue/label_precolors"}}
 												</div>
 											</div>
@@ -162,12 +153,10 @@
 						</div>
 					{{end}}
 				</div>
-
-				<div class="divider"></div>
-
-				<div class="ui cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
+				<div class="divider"{{if .Color}} style="color: {{ContrastColor .Color}} !important"{{end}}></div>
+				<div class="ui cards" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
 					{{range (index $.IssuesMap .ID)}}
-						<div class="issue-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-issue="{{.ID}}">
+						<div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}">
 							{{template "repo/issue/card" (dict "Issue" . "Page" $)}}
 						</div>
 					{{end}}
diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl
index 62d30305b3..b66d0e360a 100644
--- a/templates/repo/actions/list.tmpl
+++ b/templates/repo/actions/list.tmpl
@@ -8,9 +8,9 @@
 		<div class="ui stackable grid">
 			<div class="four wide column">
 				<div class="ui fluid vertical menu">
-					<a class="item{{if not $.CurWorkflow}} active{{end}}" href="{{$.Link}}?actor={{$.CurActor}}&status={{$.CurStatus}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a>
+					<a class="item{{if not $.CurWorkflow}} active{{end}}" href="?actor={{$.CurActor}}&status={{$.CurStatus}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a>
 					{{range .workflows}}
-						<a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="{{$.Link}}?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">{{.Entry.Name}}
+						<a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">{{.Entry.Name}}
 							{{if .ErrMsg}}
 								<span data-tooltip-content="{{.ErrMsg}}">
 									{{svg "octicon-alert" 16 "text red"}}
@@ -25,7 +25,7 @@
 				</div>
 			</div>
 			<div class="twelve wide column content">
-				<div class="ui secondary filter menu gt-je gt-df gt-ac">
+				<div class="ui secondary filter menu tw-justify-end tw-flex tw-items-center">
 					<!-- Actor -->
 					<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
 						<span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span>
@@ -35,11 +35,11 @@
 								<i class="icon">{{svg "octicon-search"}}</i>
 								<input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.actor"}}">
 							</div>
-							<a class="item{{if not $.CurActor}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&status={{$.CurStatus}}&actor=0">
+							<a class="item{{if not $.CurActor}} active{{end}}" href="?workflow={{$.CurWorkflow}}&status={{$.CurStatus}}&actor=0">
 								{{ctx.Locale.Tr "actions.runs.actors_no_select"}}
 							</a>
 							{{range .Actors}}
-								<a class="item{{if eq .ID $.CurActor}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&actor={{.ID}}&status={{$.CurStatus}}">
+								<a class="item{{if eq .ID $.CurActor}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{.ID}}&status={{$.CurStatus}}">
 									{{ctx.AvatarUtils.Avatar . 20}} {{.GetDisplayName}}
 								</a>
 							{{end}}
@@ -54,11 +54,11 @@
 								<i class="icon">{{svg "octicon-search"}}</i>
 								<input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.status"}}">
 							</div>
-							<a class="item{{if not $.CurStatus}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status=0">
+							<a class="item{{if not $.CurStatus}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status=0">
 								{{ctx.Locale.Tr "actions.runs.status_no_select"}}
 							</a>
 							{{range .StatusInfoList}}
-								<a class="item{{if eq .Status $.CurStatus}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status={{.Status}}">
+								<a class="item{{if eq .Status $.CurStatus}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status={{.Status}}">
 									{{.DisplayedStatus}}
 								</a>
 							{{end}}
@@ -66,7 +66,7 @@
 					</div>
 
 					{{if .AllowDisableOrEnableWorkflow}}
-						<button class="ui jump dropdown btn interact-bg gt-p-3">
+						<button class="ui jump dropdown btn interact-bg tw-p-2">
 							{{svg "octicon-kebab-horizontal"}}
 							<div class="menu">
 								<a class="item link-action" data-url="{{$.Link}}/{{if .CurWorkflowDisabled}}enable{{else}}disable{{end}}?workflow={{$.CurWorkflow}}&actor={{.CurActor}}&status={{$.CurStatus}}">
diff --git a/templates/repo/actions/no_workflows.tmpl b/templates/repo/actions/no_workflows.tmpl
index af1f28e8cf..009313581e 100644
--- a/templates/repo/actions/no_workflows.tmpl
+++ b/templates/repo/actions/no_workflows.tmpl
@@ -2,7 +2,7 @@
 	{{svg "octicon-no-entry" 48}}
 	<h2>{{ctx.Locale.Tr "actions.runs.no_workflows"}}</h2>
 	{{if and .CanWriteCode .CanWriteActions}}
-		<p>{{ctx.Locale.Tr "actions.runs.no_workflows.quick_start" "https://docs.gitea.com/usage/actions/quickstart/" | Safe}}</p>
+		<p>{{ctx.Locale.Tr "actions.runs.no_workflows.quick_start" "https://docs.gitea.com/usage/actions/quickstart/"}}</p>
 	{{end}}
-	<p>{{ctx.Locale.Tr "actions.runs.no_workflows.documentation" "https://docs.gitea.com/usage/actions/overview/" | Safe}}</p>
+	<p>{{ctx.Locale.Tr "actions.runs.no_workflows.documentation" "https://docs.gitea.com/usage/actions/overview/"}}</p>
 </div>
diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl
index 580fb08a9e..ac5049cf56 100644
--- a/templates/repo/actions/runs_list.tmpl
+++ b/templates/repo/actions/runs_list.tmpl
@@ -6,7 +6,7 @@
 	</div>
 	{{end}}
 	{{range .Runs}}
-		<div class="flex-item gt-ac">
+		<div class="flex-item tw-items-center">
 			<div class="flex-item-leading">
 				{{template "repo/actions/status" (dict "status" .Status.String)}}
 			</div>
@@ -28,9 +28,9 @@
 			</div>
 			<div class="flex-item-trailing">
 				{{if .RefLink}}
-					<a class="ui label gt-px-2 gt-mx-0" href="{{.RefLink}}">{{.PrettyRef}}</a>
+					<a class="ui label tw-px-1 tw-mx-0" href="{{.RefLink}}">{{.PrettyRef}}</a>
 				{{else}}
-					<span class="ui label gt-px-2 gt-mx-0">{{.PrettyRef}}</span>
+					<span class="ui label tw-px-1 tw-mx-0">{{.PrettyRef}}</span>
 				{{end}}
 			</div>
 			<div class="run-list-item-right">
diff --git a/templates/repo/actions/status.tmpl b/templates/repo/actions/status.tmpl
index 5016570142..a0e02cf8d7 100644
--- a/templates/repo/actions/status.tmpl
+++ b/templates/repo/actions/status.tmpl
@@ -12,7 +12,7 @@
 {{- $className = .className -}}
 {{- end -}}
 
-<span class="gt-df gt-ac" data-tooltip-content="{{ctx.Locale.Tr (printf "actions.status.%s" .status)}}">
+<span class="tw-flex tw-items-center" data-tooltip-content="{{ctx.Locale.Tr (printf "actions.status.%s" .status)}}">
 {{if eq .status "success"}}
 	{{svg "octicon-check-circle-fill" $size (printf "text green %s" $className)}}
 {{else if eq .status "skipped"}}
diff --git a/templates/repo/actions/view.tmpl b/templates/repo/actions/view.tmpl
index 6b07e7000a..f8b106147b 100644
--- a/templates/repo/actions/view.tmpl
+++ b/templates/repo/actions/view.tmpl
@@ -19,6 +19,7 @@
 		data-locale-status-skipped="{{ctx.Locale.Tr "actions.status.skipped"}}"
 		data-locale-status-blocked="{{ctx.Locale.Tr "actions.status.blocked"}}"
 		data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}"
+		data-locale-confirm-delete-artifact="{{ctx.Locale.Tr "confirm_delete_artifact"}}"
 		data-locale-show-timestamps="{{ctx.Locale.Tr "show_timestamps"}}"
 		data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}"
 		data-locale-show-full-screen="{{ctx.Locale.Tr "show_full_screen"}}"
diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl
index 3149f20670..a19fb66261 100644
--- a/templates/repo/activity.tmpl
+++ b/templates/repo/activity.tmpl
@@ -1,235 +1,17 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content repository commits">
 	{{template "repo/header" .}}
-	<div class="ui container">
-		<h2 class="ui header activity-header">
-			<span>{{DateTime "long" .DateFrom}} - {{DateTime "long" .DateUntil}}</span>
-			<!-- Period -->
-			<div class="ui floating dropdown jump filter">
-				<div class="ui basic compact button">
-					{{ctx.Locale.Tr "repo.activity.period.filter_label"}} <strong>{{.PeriodText}}</strong>
-					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-				</div>
-				<div class="menu">
-					<a class="{{if eq .Period "daily"}}active {{end}}item" href="{{$.RepoLink}}/activity/daily">{{ctx.Locale.Tr "repo.activity.period.daily"}}</a>
-					<a class="{{if eq .Period "halfweekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/halfweekly">{{ctx.Locale.Tr "repo.activity.period.halfweekly"}}</a>
-					<a class="{{if eq .Period "weekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/weekly">{{ctx.Locale.Tr "repo.activity.period.weekly"}}</a>
-					<a class="{{if eq .Period "monthly"}}active {{end}}item" href="{{$.RepoLink}}/activity/monthly">{{ctx.Locale.Tr "repo.activity.period.monthly"}}</a>
-					<a class="{{if eq .Period "quarterly"}}active {{end}}item" href="{{$.RepoLink}}/activity/quarterly">{{ctx.Locale.Tr "repo.activity.period.quarterly"}}</a>
-					<a class="{{if eq .Period "semiyearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/semiyearly">{{ctx.Locale.Tr "repo.activity.period.semiyearly"}}</a>
-					<a class="{{if eq .Period "yearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/yearly">{{ctx.Locale.Tr "repo.activity.period.yearly"}}</a>
-				</div>
-			</div>
-		</h2>
-		<div class="divider"></div>
-
-		{{if (or (.Permission.CanRead $.UnitTypeIssues) (.Permission.CanRead $.UnitTypePullRequests))}}
-		<h4 class="ui top attached header">{{ctx.Locale.Tr "repo.activity.overview"}}</h4>
-		<div class="ui attached segment two column grid">
-			{{if .Permission.CanRead $.UnitTypePullRequests}}
-				<div class="column">
-					{{if gt .Activity.ActivePRCount 0}}
-					<div class="stats-table">
-						<a href="#merged-pull-requests" class="table-cell tiny background purple" style="width: {{.Activity.MergedPRPerc}}{{if ne .Activity.MergedPRPerc 0}}%{{end}}"></a>
-						<a href="#proposed-pull-requests" class="table-cell tiny background green"></a>
-					</div>
-					{{else}}
-					<div class="stats-table">
-						<a class="table-cell tiny background light grey"></a>
-					</div>
-					{{end}}
-					{{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount | Safe}}
-				</div>
-			{{end}}
-			{{if .Permission.CanRead $.UnitTypeIssues}}
-				<div class="column">
-					{{if gt .Activity.ActiveIssueCount 0}}
-					<div class="stats-table">
-						<a href="#closed-issues" class="table-cell tiny background red" style="width: {{.Activity.ClosedIssuePerc}}{{if ne .Activity.ClosedIssuePerc 0}}%{{end}}"></a>
-						<a href="#new-issues" class="table-cell tiny background green"></a>
-					</div>
-					{{else}}
-					<div class="stats-table">
-						<a class="table-cell tiny background light grey"></a>
-					</div>
-					{{end}}
-					{{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount | Safe}}
-				</div>
-			{{end}}
+	<div class="ui container flex-container">
+		<div class="flex-container-nav">
+			{{template "repo/navbar" .}}
 		</div>
-		<div class="ui attached segment horizontal segments">
-			{{if .Permission.CanRead $.UnitTypePullRequests}}
-				<a href="#merged-pull-requests" class="ui attached segment text center">
-					<span class="text purple">{{svg "octicon-git-pull-request"}}</span> <strong>{{.Activity.MergedPRCount}}</strong><br>
-					{{ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.merged_prs_count_1" "repo.activity.merged_prs_count_n"}}
-				</a>
-				<a href="#proposed-pull-requests" class="ui attached segment text center">
-					<span class="text green">{{svg "octicon-git-branch"}}</span> <strong>{{.Activity.OpenedPRCount}}</strong><br>
-					{{ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.opened_prs_count_1" "repo.activity.opened_prs_count_n"}}
-				</a>
-			{{end}}
-			{{if .Permission.CanRead $.UnitTypeIssues}}
-				<a href="#closed-issues" class="ui attached segment text center">
-					<span class="text red">{{svg "octicon-issue-closed"}}</span> <strong>{{.Activity.ClosedIssueCount}}</strong><br>
-					{{ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.closed_issues_count_1" "repo.activity.closed_issues_count_n"}}
-				</a>
-				<a href="#new-issues" class="ui attached segment text center">
-					<span class="text green">{{svg "octicon-issue-opened"}}</span> <strong>{{.Activity.OpenedIssueCount}}</strong><br>
-					{{ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.new_issues_count_1" "repo.activity.new_issues_count_n"}}
-				</a>
-			{{end}}
+		<div class="flex-container-main">
+			{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
+			{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
+			{{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}}
+			{{if .PageIsRecentCommits}}{{template "repo/recent_commits" .}}{{end}}
 		</div>
-		{{end}}
-
-		{{if .Permission.CanRead $.UnitTypeCode}}
-			{{if eq .Activity.Code.CommitCountInAllBranches 0}}
-				<div class="ui center aligned segment">
-				<h4 class="ui header">{{ctx.Locale.Tr "repo.activity.no_git_activity"}}</h4>
-				</div>
-			{{end}}
-			{{if gt .Activity.Code.CommitCountInAllBranches 0}}
-				<div class="ui attached segment horizontal segments">
-					<div class="ui attached segment text">
-						{{ctx.Locale.Tr "repo.activity.git_stats_exclude_merges"}}
-						<strong>{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n" .Activity.Code.AuthorCount}}</strong>
-						{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_1" "repo.activity.git_stats_pushed_n"}}
-						<strong>{{ctx.Locale.TrN .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCount}}</strong>
-						{{ctx.Locale.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch}}
-						<strong>{{ctx.Locale.TrN .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCountInAllBranches}}</strong>
-						{{ctx.Locale.Tr "repo.activity.git_stats_push_to_all_branches"}}
-						{{ctx.Locale.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch}}
-						<strong>{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n" .Activity.Code.ChangedFiles}}</strong>
-						{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_1" "repo.activity.git_stats_files_changed_n"}}
-						{{ctx.Locale.Tr "repo.activity.git_stats_additions"}}
-						<strong class="text green">{{ctx.Locale.TrN .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n" .Activity.Code.Additions}}</strong>
-						{{ctx.Locale.Tr "repo.activity.git_stats_and_deletions"}}
-						<strong class="text red">{{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}</strong>.
-					</div>
-					<div class="ui attached segment">
-						<div id="repo-activity-top-authors-chart"></div>
-					</div>
-				</div>
-			{{end}}
-		{{end}}
-
-		{{if gt .Activity.PublishedReleaseCount 0}}
-			<h4 class="divider divider-text gt-normal-case" id="published-releases">
-				{{svg "octicon-tag" 16 "gt-mr-3"}}
-				{{ctx.Locale.Tr "repo.activity.title.releases_published_by"
-					(ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount)
-					(ctx.Locale.TrN .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.PublishedReleaseAuthorCount)
-				}}
-			</h4>
-			<div class="list">
-				{{range .Activity.PublishedReleases}}
-					<p class="desc">
-						<span class="ui green label">{{ctx.Locale.Tr "repo.activity.published_release_label"}}</span>
-						{{.TagName}}
-						{{if not .IsTag}}
-							<a class="title" href="{{$.RepoLink}}/src/{{.TagName | PathEscapeSegments}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{end}}
-						{{TimeSinceUnix .CreatedUnix ctx.Locale}}
-					</p>
-				{{end}}
-			</div>
-		{{end}}
-
-		{{if gt .Activity.MergedPRCount 0}}
-			<h4 class="divider divider-text gt-normal-case" id="merged-pull-requests">
-				{{svg "octicon-git-pull-request" 16 "gt-mr-3"}}
-				{{ctx.Locale.Tr "repo.activity.title.prs_merged_by"
-					(ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount)
-					(ctx.Locale.TrN .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.MergedPRAuthorCount)
-				}}
-			</h4>
-			<div class="list">
-				{{range .Activity.MergedPRs}}
-					<p class="desc">
-						<span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span>
-						#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{TimeSinceUnix .MergedUnix ctx.Locale}}
-					</p>
-				{{end}}
-			</div>
-		{{end}}
-
-		{{if gt .Activity.OpenedPRCount 0}}
-			<h4 class="divider divider-text gt-normal-case" id="proposed-pull-requests">
-				{{svg "octicon-git-branch" 16 "gt-mr-3"}}
-				{{ctx.Locale.Tr "repo.activity.title.prs_opened_by"
-					(ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount)
-					(ctx.Locale.TrN .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedPRAuthorCount)
-				}}
-			</h4>
-			<div class="list">
-				{{range .Activity.OpenedPRs}}
-					<p class="desc">
-						<span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span>
-						#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
-					</p>
-				{{end}}
-			</div>
-		{{end}}
-
-		{{if gt .Activity.ClosedIssueCount 0}}
-			<h4 class="divider divider-text gt-normal-case" id="closed-issues">
-				{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
-				{{ctx.Locale.Tr "repo.activity.title.issues_closed_from"
-					(ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount)
-					(ctx.Locale.TrN .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.ClosedIssueAuthorCount)
-				}}
-			</h4>
-			<div class="list">
-				{{range .Activity.ClosedIssues}}
-					<p class="desc">
-						<span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span>
-						#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{TimeSinceUnix .ClosedUnix ctx.Locale}}
-					</p>
-				{{end}}
-			</div>
-		{{end}}
-
-		{{if gt .Activity.OpenedIssueCount 0}}
-			<h4 class="divider divider-text gt-normal-case" id="new-issues">
-				{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
-				{{ctx.Locale.Tr "repo.activity.title.issues_created_by"
-					(ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount)
-					(ctx.Locale.TrN .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedIssueAuthorCount)
-				}}
-			</h4>
-			<div class="list">
-				{{range .Activity.OpenedIssues}}
-					<p class="desc">
-						<span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span>
-						#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{TimeSinceUnix .CreatedUnix ctx.Locale}}
-					</p>
-				{{end}}
-			</div>
-		{{end}}
-
-		{{if gt .Activity.UnresolvedIssueCount 0}}
-			<h4 class="divider divider-text gt-normal-case" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}">
-				{{svg "octicon-comment-discussion" 16 "gt-mr-3"}}
-				{{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}}
-			</h4>
-			<div class="list">
-				{{range .Activity.UnresolvedIssues}}
-					<p class="desc">
-						<span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span>
-						#{{.Index}}
-						{{if .IsPull}}
-						<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{else}}
-						<a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
-						{{end}}
-						{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
-					</p>
-				{{end}}
-			</div>
-		{{end}}
 	</div>
 </div>
 {{template "base/footer" .}}
+
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl
index 31cd5b23f6..30d1a3d78d 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -2,20 +2,20 @@
 	{{$revsFileLink := URLJoin .RepoLink "src" .BranchNameSubURL "/.git-blame-ignore-revs"}}
 	{{if .UsesIgnoreRevs}}
 		<div class="ui info message">
-			<p>{{ctx.Locale.Tr "repo.blame.ignore_revs" $revsFileLink (print $revsFileLink "?bypass-blame-ignore=true") | Str2html}}</p>
+			<p>{{ctx.Locale.Tr "repo.blame.ignore_revs" $revsFileLink (print $revsFileLink "?bypass-blame-ignore=true")}}</p>
 		</div>
 	{{else}}
 		<div class="ui error message">
-			<p>{{ctx.Locale.Tr "repo.blame.ignore_revs.failed" $revsFileLink | Str2html}}</p>
+			<p>{{ctx.Locale.Tr "repo.blame.ignore_revs.failed" $revsFileLink}}</p>
 		</div>
 	{{end}}
 {{end}}
 <div class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content">
-	<h4 class="file-header ui top attached header gt-df gt-ac gt-sb gt-fw">
-		<div class="file-header-left gt-df gt-ac gt-py-3 gt-pr-4">
+	<h4 class="file-header ui top attached header tw-flex tw-items-center tw-justify-between tw-flex-wrap">
+		<div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4">
 			{{template "repo/file_info" .}}
 		</div>
-		<div class="file-header-right file-actions gt-df gt-ac gt-fw">
+		<div class="file-header-right file-actions tw-flex tw-items-center tw-flex-wrap">
 			<div class="ui buttons">
 				<a class="ui tiny button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
 				{{if not .IsViewCommit}}
@@ -24,12 +24,15 @@
 				<a class="ui tiny button" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.normal_view"}}</a>
 				<a class="ui tiny button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.file_history"}}</a>
 				<button class="ui tiny button unescape-button">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
-				<button class="ui tiny button escape-button gt-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
+				<button class="ui tiny button escape-button tw-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
 			</div>
 		</div>
 	</h4>
 	<div class="ui attached table unstackable segment">
 		<div class="file-view code-view unicode-escaped">
+			{{if .IsFileTooLarge}}
+				{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
+			{{else}}
 			<table>
 				<tbody>
 					{{range $row := .BlameRows}}
@@ -41,11 +44,11 @@
 											{{$row.Avatar}}
 										</div>
 										<div class="blame-message">
-											<a href="{{$row.CommitURL}}" title="{{$row.CommitMessage}}">
+											<a class="suppressed tw-text-text" href="{{$row.CommitURL}}" title="{{$row.CommitMessage}}">
 												{{$row.CommitMessage}}
 											</a>
 										</div>
-										<div class="blame-time">
+										<div class="blame-time not-mobile">
 											{{$row.CommitSince}}
 										</div>
 									</div>
@@ -53,7 +56,7 @@
 							</td>
 							<td class="lines-blame-btn">
 								{{if $row.PreviousSha}}
-									<a href="{{$row.PreviousShaURL}}" data-tooltip-content='{{ctx.Locale.Tr "repo.blame_prior"}}'>
+									<a role="button" class="muted" href="{{$row.PreviousShaURL}}" data-tooltip-content='{{ctx.Locale.Tr "repo.blame_prior"}}'>
 										{{svg "octicon-versions"}}
 									</a>
 								{{end}}
@@ -69,12 +72,19 @@
 								</td>
 							{{end}}
 							<td rel="L{{$row.RowNumber}}" class="lines-code blame-code chroma">
-								<code class="code-inner gt-pl-3">{{$row.Code}}</code>
+								<code class="code-inner tw-pl-2">{{$row.Code}}</code>
 							</td>
 						</tr>
 					{{end}}
 				</tbody>
 			</table>
+			{{end}}{{/* end if .IsFileTooLarge */}}
+			<div class="code-line-menu tippy-target">
+				{{if $.Permission.CanRead $.UnitTypeIssues}}
+					<a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a>
+				{{end}}
+				<a class="item copy-line-permalink" role="menuitem" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a>
+			</div>
 		</div>
 	</div>
 </div>
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index 8ae7301c4a..77cccd65b7 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -22,14 +22,14 @@
 								<div class="flex-text-block">
 									{{if .DefaultBranchBranch.IsProtected}}{{svg "octicon-shield-lock"}}{{end}}
 									<a class="gt-ellipsis" href="{{.RepoLink}}/src/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{.DefaultBranchBranch.DBBranch.Name}}</a>
-									<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
+									<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}}
 								</div>
-								<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
+								<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
 							</td>
 							<td class="right aligned middle aligned overflow-visible">
 								{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}}
-									<button class="btn interact-bg show-create-branch-modal gt-p-3"
+									<button class="btn interact-bg show-create-branch-modal tw-p-2"
 										data-modal="#create-branch-modal"
 										data-branch-from="{{$.DefaultBranchBranch.DBBranch.Name}}"
 										data-branch-from-urlcomponent="{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}"
@@ -39,10 +39,10 @@
 									</button>
 								{{end}}
 								{{if .EnableFeed}}
-									<a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-rss"}}</a>
+									<a role="button" class="btn interact-bg tw-p-2" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-rss"}}</a>
 								{{end}}
 								{{if not $.DisableDownloadSourceArchives}}
-									<div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" ($.DefaultBranchBranch.DBBranch.Name)}}">
+									<div class="ui dropdown btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" ($.DefaultBranchBranch.DBBranch.Name)}}">
 										{{svg "octicon-download"}}
 										<div class="menu">
 											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;ZIP</a>
@@ -51,7 +51,7 @@
 									</div>
 								{{end}}
 								{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted) (not $.IsMirror)}}
-									<button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal"
+									<button class="btn interact-bg tw-p-2 show-modal show-rename-branch-modal"
 										data-is-default-branch="true"
 										data-modal="#rename-branch-modal"
 										data-old-branch-name="{{$.DefaultBranchBranch.DBBranch.Name}}"
@@ -67,140 +67,136 @@
 			</div>
 		{{end}}
 
-		{{if .Branches}}
-			<h4 class="ui top attached header gt-df gt-ac gt-sb">
-				<div class="gt-df gt-ac">
-					{{ctx.Locale.Tr "repo.branches"}}
-				</div>
-				<div class="gt-whitespace-nowrap">
-					<form class="ignore-dirty" method="get">
-						<div class="ui tiny search input">
-							<input name="q" placeholder="{{ctx.Locale.Tr "repo.branch.search"}}" value="{{.Keyword}}" autofocus>
-						</div>
-						<button class="ui primary tiny button gt-mr-0" data-tooltip-content={{ctx.Locale.Tr "repo.commits.search.tooltip"}}>{{ctx.Locale.Tr "repo.commits.find"}}</button>
-					</form>
-				</div>
-			</h4>
-
-			<div class="ui attached table segment">
-				<table class="ui very basic striped fixed table single line">
-					<tbody>
-						{{range .Branches}}
-							<tr>
-								<td class="eight wide">
-								{{if .DBBranch.IsDeleted}}
-									<div class="flex-text-block">
-										<a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
-										<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
-									</div>
-									<p class="info">{{ctx.Locale.Tr "repo.branch.deleted_by" .DBBranch.DeletedBy.Name}} {{TimeSinceUnix .DBBranch.DeletedUnix ctx.Locale}}</p>
-								{{else}}
-									<div class="flex-text-block">
-										{{if .IsProtected}}{{svg "octicon-shield-lock"}}{{end}}
-										<a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
-										<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
-										{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
-									</div>
-									<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
-								{{end}}
-								</td>
-								<td class="two wide ui">
-									{{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}}
-									<div class="commit-divergence">
-										<div class="bar-group">
-											<div class="count count-behind">{{.CommitsBehind}}</div>
-											{{/* old code bears 0/0.0 = NaN output, so it might output invalid "width: NaNpx", it just works and doesn't caues any problem. */}}
-											<div class="bar bar-behind" style="width: {{Eval 100 "*" .CommitsBehind "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div>
-										</div>
-										<div class="bar-group">
-											<div class="count count-ahead">{{.CommitsAhead}}</div>
-											<div class="bar bar-ahead" style="width: {{Eval 100 "*" .CommitsAhead "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div>
-										</div>
-									</div>
-									{{end}}
-								</td>
-								<td class="two wide right aligned">
-									{{if not .LatestPullRequest}}
-										{{if .IsIncluded}}
-											<span class="ui orange large label" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.included_desc"}}">
-												{{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.branch.included"}}
-											</span>
-										{{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
-										<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}">
-											<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
-										</a>
-										{{end}}
-									{{else if and .LatestPullRequest.HasMerged .MergeMovedOn}}
-										{{if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
-										<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}">
-											<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
-										</a>
-										{{end}}
-									{{else}}
-										<a href="{{.LatestPullRequest.Issue.Link}}" class="gt-vm ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a>
-										{{if .LatestPullRequest.HasMerged}}
-											<a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a>
-										{{else if .LatestPullRequest.Issue.IsClosed}}
-											<a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a>
-										{{else}}
-											<a href="{{.LatestPullRequest.Issue.Link}}" class="ui green large label">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.issues.open_title"}}</a>
-										{{end}}
-									{{end}}
-								</td>
-								<td class="three wide right aligned overflow-visible">
-									{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted)}}
-										<button class="btn interact-bg gt-p-3 show-modal show-create-branch-modal"
-											data-branch-from="{{.DBBranch.Name}}"
-											data-branch-from-urlcomponent="{{PathEscapeSegments .DBBranch.Name}}"
-											data-tooltip-content="{{ctx.Locale.Tr "repo.branch.new_branch_from" .DBBranch.Name}}"
-											data-modal="#create-branch-modal" data-name="{{.DBBranch.Name}}"
-										>
-											{{svg "octicon-git-branch"}}
-										</button>
-									{{end}}
-									{{if $.EnableFeed}}
-										<a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DBBranch.Name}}">{{svg "octicon-rss"}}</a>
-									{{end}}
-									{{if and (not .DBBranch.IsDeleted) (not $.DisableDownloadSourceArchives)}}
-										<div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" (.DBBranch.Name)}}">
-											{{svg "octicon-download"}}
-											<div class="menu">
-												<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;ZIP</a>
-												<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;TAR.GZ</a>
-											</div>
-										</div>
-									{{end}}
-									{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted) (not $.IsMirror)}}
-										<button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal"
-											data-is-default-branch="false"
-											data-old-branch-name="{{.DBBranch.Name}}"
-											data-modal="#rename-branch-modal"
-											data-tooltip-content="{{ctx.Locale.Tr "repo.branch.rename" (.DBBranch.Name)}}"
-										>
-											{{svg "octicon-pencil"}}
-										</button>
-									{{end}}
-									{{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}}
-										{{if .DBBranch.IsDeleted}}
-											<button class="btn interact-bg gt-p-3 link-action restore-branch-button" data-url="{{$.Link}}/restore?branch_id={{.DBBranch.ID}}&name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.restore" (.DBBranch.Name)}}">
-												<span class="text blue">
-													{{svg "octicon-reply"}}
-												</span>
-											</button>
-										{{else}}
-											<button class="btn interact-bg gt-p-3 delete-button delete-branch-button" data-url="{{$.Link}}/delete?name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.delete" (.DBBranch.Name)}}" data-name="{{.DBBranch.Name}}">
-												{{svg "octicon-trash"}}
-											</button>
-										{{end}}
-									{{end}}
-								</td>
-							</tr>
-						{{end}}
-					</tbody>
-				</table>
+		<h4 class="ui top attached header tw-flex tw-items-center tw-justify-between">
+			<div class="tw-flex tw-items-center">
+				{{ctx.Locale.Tr "repo.branches"}}
 			</div>
-			{{template "base/paginate" .}}
-		{{end}}
+		</h4>
+
+		<div class="ui attached segment">
+			<form class="ignore-dirty" method="get">
+				{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.branch_kind")}}
+			</form>
+		</div>
+
+		<div class="ui attached table segment">
+			<table class="ui very basic striped fixed table single line">
+				<tbody>
+					{{range .Branches}}
+						<tr>
+							<td class="eight wide">
+							{{if .DBBranch.IsDeleted}}
+								<div class="flex-text-block">
+									<a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
+									<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
+								</div>
+								<p class="info">{{ctx.Locale.Tr "repo.branch.deleted_by" .DBBranch.DeletedBy.Name}} {{TimeSinceUnix .DBBranch.DeletedUnix ctx.Locale}}</p>
+							{{else}}
+								<div class="flex-text-block">
+									{{if .IsProtected}}{{svg "octicon-shield-lock"}}{{end}}
+									<a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
+									<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
+									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
+								</div>
+								<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
+							{{end}}
+							</td>
+							<td class="two wide ui">
+								{{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}}
+								<div class="commit-divergence">
+									<div class="bar-group">
+										<div class="count count-behind">{{.CommitsBehind}}</div>
+										{{/* old code bears 0/0.0 = NaN output, so it might output invalid "width: NaNpx", it just works and doesn't caues any problem. */}}
+										<div class="bar bar-behind" style="width: {{Eval 100 "*" .CommitsBehind "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div>
+									</div>
+									<div class="bar-group">
+										<div class="count count-ahead">{{.CommitsAhead}}</div>
+										<div class="bar bar-ahead" style="width: {{Eval 100 "*" .CommitsAhead "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div>
+									</div>
+								</div>
+								{{end}}
+							</td>
+							<td class="two wide right aligned">
+								{{if not .LatestPullRequest}}
+									{{if .IsIncluded}}
+										<span class="ui orange large label" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.included_desc"}}">
+											{{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.branch.included"}}
+										</span>
+									{{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
+									<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}">
+										<button id="new-pull-request" class="ui compact basic button tw-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
+									</a>
+									{{end}}
+								{{else if and .LatestPullRequest.HasMerged .MergeMovedOn}}
+									{{if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
+									<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}">
+										<button id="new-pull-request" class="ui compact basic button tw-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button>
+									</a>
+									{{end}}
+								{{else}}
+									<a href="{{.LatestPullRequest.Issue.Link}}" class="tw-align-middle ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a>
+									{{if .LatestPullRequest.HasMerged}}
+										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a>
+									{{else if .LatestPullRequest.Issue.IsClosed}}
+										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a>
+									{{else}}
+										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui green large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.open_title"}}</a>
+									{{end}}
+								{{end}}
+							</td>
+							<td class="three wide right aligned overflow-visible">
+								{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted)}}
+									<button class="btn interact-bg tw-p-2 show-modal show-create-branch-modal"
+										data-branch-from="{{.DBBranch.Name}}"
+										data-branch-from-urlcomponent="{{PathEscapeSegments .DBBranch.Name}}"
+										data-tooltip-content="{{ctx.Locale.Tr "repo.branch.new_branch_from" .DBBranch.Name}}"
+										data-modal="#create-branch-modal" data-name="{{.DBBranch.Name}}"
+									>
+										{{svg "octicon-git-branch"}}
+									</button>
+								{{end}}
+								{{if $.EnableFeed}}
+									<a role="button" class="btn interact-bg tw-p-2" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DBBranch.Name}}">{{svg "octicon-rss"}}</a>
+								{{end}}
+								{{if and (not .DBBranch.IsDeleted) (not $.DisableDownloadSourceArchives)}}
+									<div class="ui dropdown btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" (.DBBranch.Name)}}">
+										{{svg "octicon-download"}}
+										<div class="menu">
+											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;ZIP</a>
+											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;TAR.GZ</a>
+										</div>
+									</div>
+								{{end}}
+								{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted) (not $.IsMirror)}}
+									<button class="btn interact-bg tw-p-2 show-modal show-rename-branch-modal"
+										data-is-default-branch="false"
+										data-old-branch-name="{{.DBBranch.Name}}"
+										data-modal="#rename-branch-modal"
+										data-tooltip-content="{{ctx.Locale.Tr "repo.branch.rename" (.DBBranch.Name)}}"
+									>
+										{{svg "octicon-pencil"}}
+									</button>
+								{{end}}
+								{{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}}
+									{{if .DBBranch.IsDeleted}}
+										<button class="btn interact-bg tw-p-2 link-action restore-branch-button" data-url="{{$.Link}}/restore?branch_id={{.DBBranch.ID}}&name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.restore" (.DBBranch.Name)}}">
+											<span class="text blue">
+												{{svg "octicon-reply"}}
+											</span>
+										</button>
+									{{else}}
+										<button class="btn interact-bg tw-p-2 delete-button delete-branch-button" data-url="{{$.Link}}/delete?name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.delete" (.DBBranch.Name)}}" data-name="{{.DBBranch.Name}}">
+											{{svg "octicon-trash"}}
+										</button>
+									{{end}}
+								{{end}}
+							</td>
+						</tr>
+					{{end}}
+				</tbody>
+			</table>
+		</div>
+		{{template "base/paginate" .}}
 	</div>
 </div>
 
@@ -210,7 +206,7 @@
 		{{ctx.Locale.Tr "repo.branch.delete_html"}} <span class="name"></span>
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "repo.branch.delete_desc" | Str2html}}</p>
+		<p>{{ctx.Locale.Tr "repo.branch.delete_desc"}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl
index bee5363296..6c2e08a985 100644
--- a/templates/repo/branch_dropdown.tmpl
+++ b/templates/repo/branch_dropdown.tmpl
@@ -1,7 +1,7 @@
 {{/* Attributes:
 * root
 * ContainerClasses
-* (TODO: search "branch_dropdown" in the template direcotry)
+* (TODO: search "branch_dropdown" in the template directory)
 */}}
 {{$defaultSelectedRefName := $.root.BranchName}}
 {{if and .root.IsViewTag (not .noTag)}}
@@ -56,7 +56,7 @@
 		'repoLink': {{.root.RepoLink}},
 		'treePath': {{.root.TreePath}},
 		'branchNameSubURL': {{.root.BranchNameSubURL}},
-		'noResults': {{ctx.Locale.Tr "repo.pulls.no_results"}},
+		'noResults': {{ctx.Locale.Tr "no_results_found"}},
 	};
 	{{if .release}}
 	data.release = {
@@ -70,8 +70,8 @@
 <div class="js-branch-tag-selector {{if .ContainerClasses}}{{.ContainerClasses}}{{end}}">
 	{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}
 	<div class="ui dropdown custom">
-		<button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df gt-m-0">
-			<span class="text gt-df gt-ac gt-mr-2">
+		<button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex tw-m-0">
+			<span class="text tw-flex tw-items-center tw-mr-1">
 				{{if .release}}
 					{{ctx.Locale.Tr "repo.release.compare"}}
 				{{else}}
@@ -80,7 +80,7 @@
 					{{else}}
 						{{svg "octicon-git-branch"}}
 					{{end}}
-					<strong ref="dropdownRefName" class="gt-ml-3">{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong>
+					<strong ref="dropdownRefName" class="tw-ml-2">{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong>
 				{{end}}
 			</span>
 			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
diff --git a/templates/repo/cite/cite_buttons.tmpl b/templates/repo/cite/cite_buttons.tmpl
index 9953c92c8a..426ca3858e 100644
--- a/templates/repo/cite/cite_buttons.tmpl
+++ b/templates/repo/cite/cite_buttons.tmpl
@@ -6,6 +6,6 @@ BibTeX
 </button>
 <!-- the value will be updated by initCitationFileCopyContent, the code below is used to avoid UI flicking  -->
 <input id="citation-copy-content" value="" size="1" readonly>
-<button class="ui icon button" id="citation-clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="" data-clipboard-target="#citation-copy-content">
+<button class="ui icon button" id="citation-clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy"}}" data-clipboard-target="#citation-copy-content">
 	{{svg "octicon-copy"}}
 </button>
diff --git a/templates/repo/cite/cite_modal.tmpl b/templates/repo/cite/cite_modal.tmpl
index c34c77e0c4..fb251442ca 100644
--- a/templates/repo/cite/cite_modal.tmpl
+++ b/templates/repo/cite/cite_modal.tmpl
@@ -1,16 +1,14 @@
-<div class="ui tiny modal" id="cite-repo-modal">
+<div class="ui small modal" id="cite-repo-modal">
 	<div class="header">
 		{{ctx.Locale.Tr "repo.cite_this_repo"}}
 	</div>
 	<div class="content">
 		<div class="ui stackable secondary menu">
-			<div class="fitted item">
-				<div class="ui action input" id="citation-panel">
-					{{template "repo/cite/cite_buttons" .}}
-					<a id="goto-citation-btn" class="ui basic jump icon button" href="{{$.RepoLink}}/src/{{$.BranchName}}/CITATION.cff" data-tooltip-content="{{ctx.Locale.Tr "repo.find_file.go_to_file"}}">
-						{{svg "octicon-file-moved"}}
-					</a>
-				</div>
+			<div class="ui action input" id="citation-panel">
+				{{template "repo/cite/cite_buttons" .}}
+				<a id="goto-citation-btn" class="ui basic jump icon button" href="{{$.RepoLink}}/src/{{$.BranchName}}/CITATION.cff" data-tooltip-content="{{ctx.Locale.Tr "repo.find_file.go_to_file"}}">
+					{{svg "octicon-file-moved"}}
+				</a>
 			</div>
 		</div>
 	</div>
diff --git a/templates/repo/clone_buttons.tmpl b/templates/repo/clone_buttons.tmpl
index a664c4bda8..89daba9dc9 100644
--- a/templates/repo/clone_buttons.tmpl
+++ b/templates/repo/clone_buttons.tmpl
@@ -1,15 +1,15 @@
 <!-- there is always at least one button (by context/repo.go) -->
 {{if $.CloneButtonShowHTTPS}}
-	<button class="ui small compact clone button" id="repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}">
+	<button class="ui small button" id="repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}">
 		HTTPS
 	</button>
 {{end}}
 {{if $.CloneButtonShowSSH}}
-	<button class="ui small compact clone button" id="repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">
+	<button class="ui small button" id="repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">
 		SSH
 	</button>
 {{end}}
 <input id="repo-clone-url" size="20" class="js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly>
-<button class="ui basic small compact icon button" id="clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{ctx.Locale.Tr "copy_url"}}">
+<button class="ui small icon button" id="clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{ctx.Locale.Tr "copy_url"}}">
 	{{svg "octicon-copy" 14}}
 </button>
diff --git a/templates/repo/clone_script.tmpl b/templates/repo/clone_script.tmpl
index 0797b400d8..40dae76dc7 100644
--- a/templates/repo/clone_script.tmpl
+++ b/templates/repo/clone_script.tmpl
@@ -24,19 +24,27 @@
 		const btn = isSSH ? sshBtn : httpsBtn;
 		if (!btn) return;
 
-		let link = btn.getAttribute('data-link');
-		if (link.startsWith('http://') || link.startsWith('https://')) {
-			// use current protocol/host as the clone link
-			const url = new URL(link);
-			url.protocol = window.location.protocol;
-			url.host = window.location.host;
-			link = url.toString();
+		// NOTE: Keep this function in sync with the one in the js folder
+		function toOriginUrl(urlStr) {
+			try {
+				if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) {
+					const {origin, protocol, hostname, port} = window.location;
+					const url = new URL(urlStr, origin);
+					url.protocol = protocol;
+					url.hostname = hostname;
+					url.port = port || (protocol === 'https:' ? '443' : '80');
+					return url.toString();
+				}
+			} catch {}
+			return urlStr;
 		}
+		const link = toOriginUrl(btn.getAttribute('data-link'));
+
 		for (const el of document.getElementsByClassName('js-clone-url')) {
 			el[el.nodeName === 'INPUT' ? 'value' : 'textContent'] = link;
 		}
-		for (const el of document.getElementsByClassName('js-clone-url-vsc')) {
-			el['href'] = 'vscode://vscode.git/clone?url=' + encodeURIComponent(link);
+		for (const el of document.getElementsByClassName('js-clone-url-editor')) {
+			el.href = el.getAttribute('data-href-template').replace('{url}', encodeURIComponent(link));
 		}
 	})();
 </script>
diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl
index 8910a9e5b6..17ae7d119d 100644
--- a/templates/repo/code/recently_pushed_new_branches.tmpl
+++ b/templates/repo/code/recently_pushed_new_branches.tmpl
@@ -1,10 +1,11 @@
 {{range .RecentlyPushedNewBranches}}
-	<div class="ui positive message gt-df gt-ac">
-		<div class="gt-f1">
+	<div class="ui positive message tw-flex tw-items-center">
+		<div class="tw-flex-1">
 			{{$timeSince := TimeSince .CommitTime.AsTime ctx.Locale}}
-			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" (Escape .Name) $timeSince | Safe}}
+			{{$branchLink := HTMLFormat `<a href="%s/src/branch/%s">%s</a>` $.RepoLink (PathEscapeSegments .Name) .Name}}
+			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $branchLink $timeSince}}
 		</div>
-		<a role="button" class="ui compact positive button gt-m-0" href="{{$.Repository.ComposeBranchCompareURL $.Repository.BaseRepo .Name}}">
+		<a role="button" class="ui compact positive button tw-m-0" href="{{$.Repository.ComposeBranchCompareURL $.Repository.BaseRepo .Name}}">
 			{{ctx.Locale.Tr "repo.pulls.compare_changes"}}
 		</a>
 	</div>
diff --git a/templates/repo/code_frequency.tmpl b/templates/repo/code_frequency.tmpl
new file mode 100644
index 0000000000..50ec1beb6b
--- /dev/null
+++ b/templates/repo/code_frequency.tmpl
@@ -0,0 +1,9 @@
+{{if .Permission.CanRead $.UnitTypeCode}}
+	<div id="repo-code-frequency-chart"
+		data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.code_frequency.what")}}"
+		data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.code_frequency.what")}}"
+		data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}"
+		data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
+	>
+	</div>
+{{end}}
diff --git a/templates/repo/commit_load_branches_and_tags.tmpl b/templates/repo/commit_load_branches_and_tags.tmpl
index 883230ac29..ffa0e530e8 100644
--- a/templates/repo/commit_load_branches_and_tags.tmpl
+++ b/templates/repo/commit_load_branches_and_tags.tmpl
@@ -1,19 +1,19 @@
 {{if not .PageIsWiki}}
 <div class="branch-and-tag-area" data-text-default-branch-tooltip="{{ctx.Locale.Tr "repo.commit.contained_in_default_branch"}}">
-	<button class="ui button ellipsis-button load-branches-and-tags gt-mt-3" aria-expanded="false"
+	<button class="ui button ellipsis-button load-branches-and-tags tw-mt-2" aria-expanded="false"
 		data-fetch-url="{{.RepoLink}}/commit/{{.CommitID}}/load-branches-and-tags"
 		data-tooltip-content="{{ctx.Locale.Tr "repo.commit.load_referencing_branches_and_tags"}}"
 	>...</button>
-	<div class="branch-and-tag-detail gt-hidden">
+	<div class="branch-and-tag-detail tw-hidden">
 		<div class="divider"></div>
 		<div>{{ctx.Locale.Tr "repo.commit.contained_in"}}</div>
-		<div class="gt-df gt-mt-3">
-			<div class="gt-p-2">{{svg "octicon-git-branch"}}</div>
-			<div class="branch-area flex-text-block gt-fw gt-f1"></div>
+		<div class="tw-flex tw-mt-2">
+			<div class="tw-p-1">{{svg "octicon-git-branch"}}</div>
+			<div class="branch-area flex-text-block tw-flex-wrap tw-flex-1"></div>
 		</div>
-		<div class="gt-df gt-mt-3">
-			<div class="gt-p-2">{{svg "octicon-tag"}}</div>
-			<div class="tag-area flex-text-block gt-fw gt-f1"></div>
+		<div class="tw-flex tw-mt-2">
+			<div class="tw-p-1">{{svg "octicon-tag"}}</div>
+			<div class="tag-area flex-text-block tw-flex-wrap tw-flex-1"></div>
 		</div>
 	</div>
 </div>
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 01fa45babe..7fec88cb79 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -17,11 +17,11 @@
 				{{$class = (print $class " isWarning")}}
 			{{end}}
 		{{end}}
-		<div class="ui top attached header clearing segment gt-relative commit-header {{$class}}">
-			<div class="gt-df gt-mb-4 gt-fw">
-				<h3 class="gt-mb-0 gt-f1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
+		<div class="ui top attached header clearing segment tw-relative commit-header {{$class}}">
+			<div class="tw-flex tw-mb-4 tw-gap-1">
+				<h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
 				{{if not $.PageIsWiki}}
-					<div>
+					<div class="commit-header-buttons">
 						<a class="ui primary tiny button" href="{{.SourcePath}}">
 							{{ctx.Locale.Tr "repo.diff.browse_source"}}
 						</a>
@@ -88,7 +88,7 @@
 												{{.CsrfTokenHtml}}
 												<div class="field">
 													<label>
-														{{ctx.Locale.Tr "repo.branch.new_branch_from" `<span class="text" id="modal-create-branch-from-span"></span>` | Safe}}
+														{{ctx.Locale.Tr "repo.branch.new_branch_from" (`<span class="text" id="modal-create-branch-from-span"></span>`|SafeHTML)}}
 													</label>
 												</div>
 												<div class="required field">
@@ -113,7 +113,7 @@
 												<input type="hidden" name="create_tag" value="true">
 												<div class="field">
 													<label>
-														{{ctx.Locale.Tr "repo.tag.create_tag_from" `<span class="text" id="modal-create-tag-from-span"></span>` | Safe}}
+														{{ctx.Locale.Tr "repo.tag.create_tag_from" (`<span class="text" id="modal-create-tag-from-span"></span>`|SafeHTML)}}
 													</label>
 												</div>
 												<div class="required field">
@@ -139,34 +139,34 @@
 			{{end}}
 			{{template "repo/commit_load_branches_and_tags" .}}
 		</div>
-		<div class="ui attached segment gt-df gt-ac gt-sb gt-py-2 commit-header-row gt-fw {{$class}}">
-				<div class="gt-df gt-ac author">
+		<div class="ui attached segment tw-flex tw-items-center tw-justify-between tw-py-1 commit-header-row tw-flex-wrap {{$class}}">
+				<div class="tw-flex tw-items-center author">
 					{{if .Author}}
-						{{ctx.AvatarUtils.Avatar .Author 28 "gt-mr-3"}}
+						{{ctx.AvatarUtils.Avatar .Author 28 "tw-mr-2"}}
 						{{if .Author.FullName}}
 							<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong></a>
 						{{else}}
 							<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong></a>
 						{{end}}
 					{{else}}
-						{{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "gt-mr-3"}}
+						{{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "tw-mr-2"}}
 						<strong>{{.Commit.Author.Name}}</strong>
 					{{end}}
-					<span class="text grey gt-ml-3" id="authored-time">{{TimeSince .Commit.Author.When ctx.Locale}}</span>
+					<span class="text grey tw-ml-2" id="authored-time">{{TimeSince .Commit.Author.When ctx.Locale}}</span>
 					{{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}}
-						<span class="text grey gt-mx-3">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span>
+						<span class="text grey tw-mx-2">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span>
 						{{if ne .Verification.CommittingUser.ID 0}}
-							{{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "gt-mx-3"}}
+							{{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "tw-mx-2"}}
 							<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a>
 						{{else}}
-							{{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "gt-mr-3"}}
+							{{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "tw-mr-2"}}
 							<strong>{{.Commit.Committer.Name}}</strong>
 						{{end}}
 					{{end}}
 				</div>
-				<div class="ui horizontal list gt-df gt-ac">
+				<div class="tw-flex tw-items-center">
 					{{if .Parents}}
-						<div class="item">
+						<div>
 							<span>{{ctx.Locale.Tr "repo.diff.parent"}}</span>
 							{{range .Parents}}
 								{{if $.PageIsWiki}}
@@ -184,73 +184,73 @@
 				</div>
 		</div>
 		{{if .Commit.Signature}}
-			<div class="ui bottom attached message gt-text-left gt-df gt-ac gt-sb commit-header-row gt-fw {{$class}}">
-				<div class="gt-df gt-ac">
+			<div class="ui bottom attached message tw-text-left tw-flex tw-items-center tw-justify-between commit-header-row tw-flex-wrap tw-mb-0 {{$class}}">
+				<div class="tw-flex tw-items-center">
 					{{if .Verification.Verified}}
 						{{if ne .Verification.SigningUser.ID 0}}
-							{{svg "gitea-lock" 16 "gt-mr-3"}}
+							{{svg "gitea-lock" 16 "tw-mr-2"}}
 							{{if eq .Verification.TrustStatus "trusted"}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span>
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span>
 							{{else if eq .Verification.TrustStatus "untrusted"}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}:</span>
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}:</span>
 							{{else}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span>
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span>
 							{{end}}
-							{{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "gt-mr-3"}}
+							{{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "tw-mr-2"}}
 							<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.GetDisplayName}}</strong></a>
 						{{else}}
-							<span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog" 16 "gt-mr-3"}}</span>
-							<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span>
-							{{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "gt-mr-3"}}
+							<span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog" 16 "tw-mr-2"}}</span>
+							<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span>
+							{{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "tw-mr-2"}}
 							<strong>{{.Verification.SigningUser.GetDisplayName}}</strong>
 						{{end}}
 					{{else}}
-						{{svg "gitea-unlock" 16 "gt-mr-3"}}
+						{{svg "gitea-unlock" 16 "tw-mr-2"}}
 						<span class="ui text">{{ctx.Locale.Tr .Verification.Reason}}</span>
 					{{end}}
 				</div>
-				<div class="gt-df gt-ac">
+				<div class="tw-flex tw-items-center">
 					{{if .Verification.Verified}}
 						{{if ne .Verification.SigningUser.ID 0}}
-							{{svg "octicon-verified" 16 "gt-mr-3"}}
+							{{svg "octicon-verified" 16 "tw-mr-2"}}
 							{{if .Verification.SigningSSHKey}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
 								{{.Verification.SigningSSHKey.Fingerprint}}
 							{{else}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
 								{{.Verification.SigningKey.PaddedKeyID}}
 							{{end}}
 						{{else}}
-							{{svg "octicon-unverified" 16 "gt-mr-3"}}
+							{{svg "octicon-unverified" 16 "tw-mr-2"}}
 							{{if .Verification.SigningSSHKey}}
-								<span class="ui text gt-mr-3" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
+								<span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
 								{{.Verification.SigningSSHKey.Fingerprint}}
 							{{else}}
-								<span class="ui text gt-mr-3" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
+								<span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
 								{{.Verification.SigningKey.PaddedKeyID}}
 							{{end}}
 						{{end}}
 					{{else if .Verification.Warning}}
-						{{svg "octicon-unverified" 16 "gt-mr-3"}}
+						{{svg "octicon-unverified" 16 "tw-mr-2"}}
 						{{if .Verification.SigningSSHKey}}
-							<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
+							<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
 							{{.Verification.SigningSSHKey.Fingerprint}}
 						{{else}}
-							<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
+							<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
 							{{.Verification.SigningKey.PaddedKeyID}}
 						{{end}}
 					{{else}}
 						{{if .Verification.SigningKey}}
 							{{if ne .Verification.SigningKey.KeyID ""}}
-								{{svg "octicon-verified" 16 "gt-mr-3"}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
+								{{svg "octicon-verified" 16 "tw-mr-2"}}
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
 								{{.Verification.SigningKey.PaddedKeyID}}
 							{{end}}
 						{{end}}
 						{{if .Verification.SigningSSHKey}}
 							{{if ne .Verification.SigningSSHKey.Fingerprint ""}}
-								{{svg "octicon-verified" 16 "gt-mr-3"}}
-								<span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
+								{{svg "octicon-verified" 16 "tw-mr-2"}}
+								<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
 								{{.Verification.SigningSSHKey.Fingerprint}}
 							{{end}}
 						{{end}}
@@ -260,7 +260,7 @@
 		{{end}}
 		{{if .NoteRendered}}
 			<div class="ui top attached header segment git-notes">
-				{{svg "octicon-note" 16 "gt-mr-3"}}
+				{{svg "octicon-note" 16 "tw-mr-2"}}
 				{{ctx.Locale.Tr "repo.diff.git-notes"}}:
 				{{if .NoteAuthor}}
 					<a href="{{.NoteAuthor.HomeLink}}">
@@ -276,7 +276,7 @@
 				<span class="text grey" id="note-authored-time">{{TimeSince .NoteCommit.Author.When ctx.Locale}}</span>
 			</div>
 			<div class="ui bottom attached info segment git-notes">
-				<pre class="commit-body">{{.NoteRendered | Str2html}}</pre>
+				<pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre>
 			</div>
 		{{end}}
 		{{template "repo/diff/box" .}}
diff --git a/templates/repo/commit_statuses.tmpl b/templates/repo/commit_statuses.tmpl
index ec2be6c38d..f451ac06a1 100644
--- a/templates/repo/commit_statuses.tmpl
+++ b/templates/repo/commit_statuses.tmpl
@@ -1,10 +1,10 @@
 {{if .Statuses}}
 	{{if and (eq (len .Statuses) 1) .Status.TargetURL}}
-		<a class="gt-vm gt-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
+		<a class="tw-align-middle {{.AdditionalClasses}} tw-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
 			{{template "repo/commit_status" .Status}}
 		</a>
 	{{else}}
-		<span class="gt-vm" data-tippy="commit-statuses" tabindex="0">
+		<span class="tw-align-middle {{.AdditionalClasses}}" data-tippy="commit-statuses" tabindex="0">
 			{{template "repo/commit_status" .Status}}
 		</span>
 	{{end}}
diff --git a/templates/repo/commits.tmpl b/templates/repo/commits.tmpl
index 42004c2610..e6efe1ff54 100644
--- a/templates/repo/commits.tmpl
+++ b/templates/repo/commits.tmpl
@@ -4,8 +4,8 @@
 	<div class="ui container">
 		{{template "repo/sub_menu" .}}
 		<div class="repo-button-row">
-			<div class="gt-df gt-ac">
-				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "gt-mr-2"}}
+			<div class="tw-flex tw-items-center">
+				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}}
 				<a href="{{.RepoLink}}/graph" class="ui basic small compact button">
 					{{svg "octicon-git-branch"}}
 					{{ctx.Locale.Tr "repo.commit_graph"}}
diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl
index 7702770c40..bb5d2a0394 100644
--- a/templates/repo/commits_list.tmpl
+++ b/templates/repo/commits_list.tmpl
@@ -1,89 +1,93 @@
 <div class="ui attached table segment commit-table">
-		<table class="ui very basic striped table unstackable" id="commits-table">
-			<thead>
+	<table class="ui very basic striped table unstackable" id="commits-table">
+		<thead>
+			<tr>
+				<th class="three wide">{{ctx.Locale.Tr "repo.commits.author"}}</th>
+				<th class="two wide sha">{{StringUtils.ToUpper $.Repository.ObjectFormatName}}</th>
+				<th class="eight wide message">{{ctx.Locale.Tr "repo.commits.message"}}</th>
+				<th class="two wide right aligned">{{ctx.Locale.Tr "repo.commits.date"}}</th>
+				<th class="one wide"></th>
+			</tr>
+		</thead>
+		<tbody class="commit-list">
+			{{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}}
+			{{range .Commits}}
 				<tr>
-					<th class="three wide">{{ctx.Locale.Tr "repo.commits.author"}}</th>
-					<th class="two wide sha">{{StringUtils.ToUpper $.Repository.ObjectFormatName}}</th>
-					<th class="eight wide message">{{ctx.Locale.Tr "repo.commits.message"}}</th>
-					<th class="two wide right aligned">{{ctx.Locale.Tr "repo.commits.date"}}</th>
-					<th class="one wide"></th>
-				</tr>
-			</thead>
-			<tbody class="commit-list">
-				{{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}}
-				{{range .Commits}}
-					<tr>
-						<td class="author">
+					<td class="author">
+						<div class="tw-flex">
 							{{$userName := .Author.Name}}
 							{{if .User}}
-								{{if .User.FullName}}
+								{{if and .User.FullName DefaultShowFullName}}
 									{{$userName = .User.FullName}}
 								{{end}}
-								{{ctx.AvatarUtils.Avatar .User 28 "gt-mr-2"}}<a href="{{.User.HomeLink}}">{{$userName}}</a>
+								{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}<a class="muted author-wrapper" href="{{.User.HomeLink}}">{{$userName}}</a>
 							{{else}}
-								{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "gt-mr-2"}}
-								{{$userName}}
+								{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-2"}}
+								<span class="author-wrapper">{{$userName}}</span>
 							{{end}}
-						</td>
-						<td class="sha">
-							{{$class := "ui sha label"}}
-							{{if .Signature}}
-								{{$class = (print $class " isSigned")}}
-								{{if .Verification.Verified}}
-									{{if eq .Verification.TrustStatus "trusted"}}
-										{{$class = (print $class " isVerified")}}
-									{{else if eq .Verification.TrustStatus "untrusted"}}
-										{{$class = (print $class " isVerifiedUntrusted")}}
-									{{else}}
-										{{$class = (print $class " isVerifiedUnmatched")}}
-									{{end}}
-								{{else if .Verification.Warning}}
-									{{$class = (print $class " isWarning")}}
+						</div>
+					</td>
+					<td class="sha">
+						{{$class := "ui sha label"}}
+						{{if .Signature}}
+							{{$class = (print $class " isSigned")}}
+							{{if .Verification.Verified}}
+								{{if eq .Verification.TrustStatus "trusted"}}
+									{{$class = (print $class " isVerified")}}
+								{{else if eq .Verification.TrustStatus "untrusted"}}
+									{{$class = (print $class " isVerifiedUntrusted")}}
+								{{else}}
+									{{$class = (print $class " isVerifiedUnmatched")}}
 								{{end}}
+							{{else if .Verification.Warning}}
+								{{$class = (print $class " isWarning")}}
 							{{end}}
-							{{$commitShaLink := ""}}
-							{{if $.PageIsWiki}}
-								{{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
-							{{else if $.PageIsPullCommits}}
-								{{$commitShaLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}}
-							{{else if $.Reponame}}
-								{{$commitShaLink = (printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
-							{{end}}
-							<a {{if $commitShaLink}}href="{{$commitShaLink}}"{{end}} class="{{$class}}">
-								<span class="shortsha">{{ShortSha .ID.String}}</span>
-								{{if .Signature}}{{template "repo/shabox_badge" dict "root" $ "verification" .Verification}}{{end}}
-							</a>
-						</td>
-						<td class="message">
-							<span class="message-wrapper">
-							{{if $.PageIsWiki}}
-								<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | RenderEmoji $.Context}}</span>
-							{{else}}
-								{{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
-								<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.Context .Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
-							{{end}}
-							</span>
-							{{if IsMultilineCommitMessage .Message}}
-							<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
-							{{end}}
-							{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
-							{{if IsMultilineCommitMessage .Message}}
-							<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre>
-							{{end}}
-						</td>
-						{{if .Committer}}
-							<td class="text right aligned">{{TimeSince .Committer.When ctx.Locale}}</td>
-						{{else}}
-							<td class="text right aligned">{{TimeSince .Author.When ctx.Locale}}</td>
 						{{end}}
-						<td class="text right aligned gt-py-0">
-							<button class="btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button>
-							{{if $.FileName}}
-								<a class="btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}" href="{{printf "%s/src/commit/%s/%s" $commitRepoLink (PathEscape .ID.String) (PathEscapeSegments $.FileName)}}">{{svg "octicon-file-code"}}</a>
-							{{end}}
-						</td>
-					</tr>
-				{{end}}
-			</tbody>
-		</table>
-	</div>
+						{{$commitShaLink := ""}}
+						{{if $.PageIsWiki}}
+							{{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
+						{{else if $.PageIsPullCommits}}
+							{{$commitShaLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}}
+						{{else if $.Reponame}}
+							{{$commitShaLink = (printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
+						{{end}}
+						<a {{if $commitShaLink}}href="{{$commitShaLink}}"{{end}} class="{{$class}}">
+							<span class="shortsha">{{ShortSha .ID.String}}</span>
+							{{if .Signature}}{{template "repo/shabox_badge" dict "root" $ "verification" .Verification}}{{end}}
+						</a>
+					</td>
+					<td class="message">
+						<span class="message-wrapper">
+						{{if $.PageIsWiki}}
+							<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | RenderEmoji $.Context}}</span>
+						{{else}}
+							{{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
+							<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.Context .Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
+						{{end}}
+						</span>
+						{{if IsMultilineCommitMessage .Message}}
+						<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
+						{{end}}
+						{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
+						{{if IsMultilineCommitMessage .Message}}
+						<pre class="commit-body tw-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre>
+						{{end}}
+					</td>
+					{{if .Committer}}
+						<td class="text right aligned">{{TimeSince .Committer.When ctx.Locale}}</td>
+					{{else}}
+						<td class="text right aligned">{{TimeSince .Author.When ctx.Locale}}</td>
+					{{end}}
+					<td class="text right aligned tw-py-0">
+						<button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button>
+						{{if not $.PageIsWiki}}{{/* at the moment, wiki doesn't support "view at history point*/}}
+							{{$viewCommitLink := printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
+							{{if $.FileName}}{{$viewCommitLink = printf "%s/%s" $viewCommitLink (PathEscapeSegments $.FileName)}}{{end}}
+							<a class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}" href="{{$viewCommitLink}}">{{svg "octicon-file-code"}}</a>
+						{{end}}
+					</td>
+				</tr>
+			{{end}}
+		</tbody>
+	</table>
+</div>
diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl
index 79e1bd6309..d96b314d01 100644
--- a/templates/repo/commits_list_small.tmpl
+++ b/templates/repo/commits_list_small.tmpl
@@ -13,7 +13,7 @@
 
 		{{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}}
 
-		<span class="shabox gt-df gt-ac gt-float-right">
+		<span class="shabox tw-flex tw-items-center tw-float-right">
 			{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
 			{{$class := "ui sha label"}}
 			{{if .Signature}}
@@ -30,7 +30,7 @@
 					{{$class = (print $class " isWarning")}}
 				{{end}}
 			{{end}}
-			<a href="{{$commitLink}}" rel="nofollow" class="gt-ml-3 {{$class}}">
+			<a href="{{$commitLink}}" rel="nofollow" class="tw-ml-2 {{$class}}">
 				<span class="shortsha">{{ShortSha .ID.String}}</span>
 				{{if .Signature}}
 					{{template "repo/shabox_badge" dict "root" $.root "verification" .Verification}}
@@ -38,12 +38,12 @@
 			</a>
 		</span>
 
-		<span class="gt-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</span>
+		<span class="tw-font-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</span>
 		{{if IsMultilineCommitMessage .Message}}
 			<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
 		{{end}}
 		{{if IsMultilineCommitMessage .Message}}
-			<pre class="commit-body gt-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</pre>
+			<pre class="commit-body tw-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</pre>
 		{{end}}
 	</div>
 {{end}}
diff --git a/templates/repo/commits_search_dropdown.tmpl b/templates/repo/commits_search_dropdown.tmpl
new file mode 100644
index 0000000000..5aa3f4f320
--- /dev/null
+++ b/templates/repo/commits_search_dropdown.tmpl
@@ -0,0 +1,8 @@
+<div class="ui small dropdown selection">
+	<input name="all" type="hidden" value="{{.All}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+	<div class="text">{{if .All}}{{ctx.Locale.Tr "repo.commits.search_all"}}{{else}}{{ctx.Locale.Tr "repo.commits.search_branch"}}{{end}}</div>
+	<div class="menu">
+		<div class="item" data-value="false">{{ctx.Locale.Tr "repo.commits.search_branch"}}</div>
+		<div class="item" data-value="true">{{ctx.Locale.Tr "repo.commits.search_all"}}</div>
+	</div>
+</div>
diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl
index 054a3f6bec..91fc1c2fae 100644
--- a/templates/repo/commits_table.tmpl
+++ b/templates/repo/commits_table.tmpl
@@ -1,5 +1,5 @@
-<h4 class="ui top attached header commits-table gt-df gt-ac gt-sb">
-	<div class="commits-table-left gt-df gt-ac">
+<h4 class="ui top attached header commits-table tw-flex tw-items-center tw-justify-between">
+	<div class="commits-table-left tw-flex tw-items-center">
 		{{if or .PageIsCommits (gt .CommitCount 0)}}
 			{{.CommitCount}} {{ctx.Locale.Tr "repo.commits.commits"}}
 		{{else if .IsNothingToCompare}}
@@ -8,27 +8,27 @@
 			{{ctx.Locale.Tr "repo.commits.no_commits" $.BaseBranch $.HeadBranch}}
 		{{end}}
 	</div>
-	<div class="commits-table-right gt-whitespace-nowrap">
-		{{if .PageIsCommits}}
-			<form class="ignore-dirty" action="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/search">
-				<div class="ui tiny search input">
-					<input name="q" placeholder="{{ctx.Locale.Tr "repo.commits.search"}}" value="{{.Keyword}}" autofocus>
-				</div>
-
-				<div class="ui tiny checkbox">
-					<input type="checkbox" name="all" value="true" {{.All}}>
-					<label>{{ctx.Locale.Tr "repo.commits.search_all"}}</label>
-				</div>
-				<button class="ui primary tiny button gt-mr-0" data-panel="#add-deploy-key-panel" data-tooltip-content={{ctx.Locale.Tr "repo.commits.search.tooltip"}}>{{ctx.Locale.Tr "repo.commits.find"}}</button>
-			</form>
-		{{else if .IsDiffCompare}}
-			<a href="{{$.CommitRepoLink}}/commit/{{.BeforeCommitID | PathEscape}}" class="ui green sha label gt-mx-0">{{if not .BaseIsCommit}}{{if .BaseIsBranch}}{{svg "octicon-git-branch"}}{{else if .BaseIsTag}}{{svg "octicon-tag"}}{{end}}{{.BaseBranch}}{{else}}{{ShortSha .BaseBranch}}{{end}}</a>
+	{{if .IsDiffCompare}}
+		<div class="commits-table-right tw-whitespace-nowrap">
+			<a href="{{$.CommitRepoLink}}/commit/{{.BeforeCommitID | PathEscape}}" class="ui green sha label tw-mx-0">{{if not .BaseIsCommit}}{{if .BaseIsBranch}}{{svg "octicon-git-branch"}}{{else if .BaseIsTag}}{{svg "octicon-tag"}}{{end}}{{.BaseBranch}}{{else}}{{ShortSha .BaseBranch}}{{end}}</a>
 			...
-			<a href="{{$.CommitRepoLink}}/commit/{{.AfterCommitID | PathEscape}}" class="ui green sha label gt-mx-0">{{if not .HeadIsCommit}}{{if .HeadIsBranch}}{{svg "octicon-git-branch"}}{{else if .HeadIsTag}}{{svg "octicon-tag"}}{{end}}{{.HeadBranch}}{{else}}{{ShortSha .HeadBranch}}{{end}}</a>
-		{{end}}
-	</div>
+			<a href="{{$.CommitRepoLink}}/commit/{{.AfterCommitID | PathEscape}}" class="ui green sha label tw-mx-0">{{if not .HeadIsCommit}}{{if .HeadIsBranch}}{{svg "octicon-git-branch"}}{{else if .HeadIsTag}}{{svg "octicon-tag"}}{{end}}{{.HeadBranch}}{{else}}{{ShortSha .HeadBranch}}{{end}}</a>
+		</div>
+	{{end}}
 </h4>
 
+{{if .PageIsCommits}}
+	<div class="ui attached segment">
+		<form class="ignore-dirty" action="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/search">
+			<div class="ui small fluid action input">
+				{{template "shared/search/input" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.commit_kind")}}
+				{{template "repo/commits_search_dropdown" .}}
+				{{template "shared/search/button" dict "Tooltip" (ctx.Locale.Tr "repo.commits.search.tooltip")}}
+			</div>
+		</form>
+	</div>
+{{end}}
+
 {{if and .Commits (gt .CommitCount 0)}}
 	{{template "repo/commits_list" .}}
 {{end}}
diff --git a/templates/repo/contributors.tmpl b/templates/repo/contributors.tmpl
new file mode 100644
index 0000000000..4a258e5b70
--- /dev/null
+++ b/templates/repo/contributors.tmpl
@@ -0,0 +1,13 @@
+{{if .Permission.CanRead $.UnitTypeCode}}
+	<div id="repo-contributors-chart"
+		data-locale-filter-label="{{ctx.Locale.Tr "repo.contributors.contribution_type.filter_label"}}"
+		data-locale-contribution-type-commits="{{ctx.Locale.Tr "repo.contributors.contribution_type.commits"}}"
+		data-locale-contribution-type-additions="{{ctx.Locale.Tr "repo.contributors.contribution_type.additions"}}"
+		data-locale-contribution-type-deletions="{{ctx.Locale.Tr "repo.contributors.contribution_type.deletions"}}"
+		data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.contributors.what")}}"
+		data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.contributors.what")}}"
+		data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}"
+		data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
+	>
+	</div>
+{{end}}
diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl
index 66f73fb398..bcd3c16b6a 100644
--- a/templates/repo/create.tmpl
+++ b/templates/repo/create.tmpl
@@ -51,10 +51,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 						<span class="help">{{ctx.Locale.Tr "repo.visibility_description"}}</span>
@@ -73,7 +73,7 @@
 						</div>
 					</div>
 
-					<div id="template_units" class="gt-hidden">
+					<div id="template_units" class="tw-hidden">
 						<div class="inline field">
 							<label>{{ctx.Locale.Tr "repo.template.items"}}</label>
 							<div class="ui checkbox">
@@ -158,7 +158,7 @@
 									{{end}}
 								</div>
 							</div>
-							<span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/" | Str2html}}</span>
+							<span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/"}}</span>
 						</div>
 
 						<div class="inline field">
diff --git a/templates/repo/create_helper.tmpl b/templates/repo/create_helper.tmpl
index 653955efc9..70c28b72e8 100644
--- a/templates/repo/create_helper.tmpl
+++ b/templates/repo/create_helper.tmpl
@@ -1,3 +1,3 @@
 {{if not $.DisableMigrations}}
-	<p class="ui center">{{ctx.Locale.Tr "repo.new_repo_helper" ((print AppSubUrl "/repo/migrate")|Escape) | Safe}}</p>
+	<p class="ui center">{{ctx.Locale.Tr "repo.new_repo_helper" (print AppSubUrl "/repo/migrate")}}</p>
 {{end}}
diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl
index 2dff28a965..8312b5d913 100644
--- a/templates/repo/diff/blob_excerpt.tmpl
+++ b/templates/repo/diff/blob_excerpt.tmpl
@@ -3,19 +3,19 @@
 	<tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}">
 		{{if eq .GetType 4}}
 			<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}">
-				<div class="gt-df">
+				<div class="tw-flex">
 				{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
-					<button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}">
+					<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 						{{svg "octicon-fold-down"}}
 					</button>
 				{{end}}
 				{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}}
-					<button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}">
+					<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 						{{svg "octicon-fold-up"}}
 					</button>
 				{{end}}
 				{{if eq $line.GetExpandDirection 2}}
-					<button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}">
+					<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 						{{svg "octicon-fold"}}
 					</button>
 				{{end}}
@@ -27,7 +27,7 @@
 			{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}
 			<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$.FileNameHash}}L{{$line.LeftIdx}}{{end}}"></span></td>
 			<td class="blob-excerpt lines-escape lines-escape-old">{{if and $line.LeftIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td>
-			<td class="blob-excerpt lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="gt-mono" data-type-marker=""></span>{{end}}</td>
+			<td class="blob-excerpt lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td>
 			<td class="blob-excerpt lines-code lines-code-old">{{/*
 				*/}}{{if $line.LeftIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/*
 					*/}}<code class="code-inner"></code>{{/*
@@ -35,7 +35,7 @@
 			*/}}</td>
 			<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$.FileNameHash}}R{{$line.RightIdx}}{{end}}"></span></td>
 			<td class="blob-excerpt lines-escape lines-escape-new">{{if and $line.RightIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td>
-			<td class="blob-excerpt lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="gt-mono" data-type-marker=""></span>{{end}}</td>
+			<td class="blob-excerpt lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td>
 			<td class="blob-excerpt lines-code lines-code-new">{{/*
 				*/}}{{if $line.RightIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/*
 					*/}}<code class="code-inner"></code>{{/*
@@ -49,19 +49,19 @@
 	<tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}">
 		{{if eq .GetType 4}}
 			<td colspan="2" class="lines-num">
-				<div class="gt-df">
+				<div class="tw-flex">
 					{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
-						<button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}">
+						<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?data-query={{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 							{{svg "octicon-fold-down"}}
 						</button>
 					{{end}}
 					{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}}
-						<button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}">
+						<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?data-query={{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 							{{svg "octicon-fold-up"}}
 						</button>
 					{{end}}
 					{{if eq $line.GetExpandDirection 2}}
-						<button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}">
+						<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?data-query={{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}">
 							{{svg "octicon-fold"}}
 						</button>
 					{{end}}
@@ -73,7 +73,7 @@
 		{{end}}
 		{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}
 		<td class="blob-excerpt lines-escape">{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td>
-		<td class="blob-excerpt lines-type-marker"><span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
+		<td class="blob-excerpt lines-type-marker"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
 		<td class="blob-excerpt lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}"><code {{if $inlineDiff.EscapeStatus.Escaped}}class="code-inner has-escaped" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"{{else}}class="code-inner"{{end}}>{{$inlineDiff.Content}}</code></td>
 	</tr>
 	{{end}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index be7c7e80f2..92a3163642 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -1,31 +1,31 @@
 {{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 1))}}
 <div>
 	<div class="diff-detail-box diff-box">
-		<div class="gt-df gt-ac gt-fw">
+		<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-ml-0.5">
 			{{if $showFileTree}}
 				<button class="diff-toggle-file-tree-button not-mobile btn interact-fg" data-show-text="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
 					{{/* the icon meaning is reversed here, "octicon-sidebar-collapse" means show the file tree */}}
-					{{svg "octicon-sidebar-collapse" 20 "icon gt-hidden"}}
-					{{svg "octicon-sidebar-expand" 20 "icon gt-hidden"}}
+					{{svg "octicon-sidebar-collapse" 20 "icon tw-hidden"}}
+					{{svg "octicon-sidebar-expand" 20 "icon tw-hidden"}}
 				</button>
 				<script>
 					// Default to true if unset
 					const diffTreeVisible = localStorage?.getItem('diff_file_tree_visible') !== 'false';
 					const diffTreeBtn = document.querySelector('.diff-toggle-file-tree-button');
 					const diffTreeIcon = `.octicon-sidebar-${diffTreeVisible ? 'expand' : 'collapse'}`;
-					diffTreeBtn.querySelector(diffTreeIcon).classList.remove('gt-hidden');
+					diffTreeBtn.querySelector(diffTreeIcon).classList.remove('tw-hidden');
 					diffTreeBtn.setAttribute('data-tooltip-content', diffTreeBtn.getAttribute(diffTreeVisible ? 'data-hide-text' : 'data-show-text'));
 				</script>
 			{{end}}
 			{{if not .DiffNotAvailable}}
-				<div class="diff-detail-stats gt-df gt-ac gt-fw">
-					{{svg "octicon-diff" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2html}}
+				<div class="diff-detail-stats tw-flex tw-items-center tw-flex-wrap">
+					{{svg "octicon-diff" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion}}
 				</div>
 			{{end}}
 		</div>
 		<div class="diff-detail-actions">
 			{{if and .PageIsPullFiles $.SignedUserID (not .IsArchived) (not .DiffNotAvailable)}}
-				<div class="not-mobile gt-df gt-ac gt-fc gt-whitespace-nowrap gt-mr-2">
+				<div class="not-mobile tw-flex tw-items-center tw-flex-col tw-whitespace-nowrap tw-mr-1">
 					<label for="viewed-files-summary" id="viewed-files-summary-label" data-text-changed-template="{{ctx.Locale.Tr "repo.pulls.viewed_files_label"}}">
 						{{ctx.Locale.Tr "repo.pulls.viewed_files_label" .Diff.NumViewedFiles .Diff.NumFiles}}
 					</label>
@@ -68,7 +68,7 @@
 				binaryFileMessage: "{{ctx.Locale.Tr "repo.diff.bin"}}",
 				showMoreMessage: "{{ctx.Locale.Tr "repo.diff.show_more"}}",
 				statisticsMessage: "{{ctx.Locale.Tr "repo.diff.stats_desc_file"}}",
-				linkLoadMore: "{{$.Link}}?skip-to={{.Diff.End}}&file-only=true",
+				linkLoadMore: "?skip-to={{.Diff.End}}&file-only=true",
 			};
 
 			// for first time loading, the diffFileInfo is a plain object
@@ -89,9 +89,9 @@
 	{{end}}
 	<div id="diff-container">
 		{{if $showFileTree}}
-			<div id="diff-file-tree" class="gt-hidden not-mobile"></div>
+			<div id="diff-file-tree" class="tw-hidden not-mobile"></div>
 			<script>
-				if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('gt-hidden');
+				if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');
 			</script>
 		{{end}}
 		{{if .DiffNotAvailable}}
@@ -109,42 +109,44 @@
 					{{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}}
 					{{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}}
 					{{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.IsArchived) $.IsShowingAllCommits}}
-					<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} gt-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
-						<h4 class="diff-file-header sticky-2nd-row ui top attached normal header gt-df gt-ac gt-sb gt-fw">
-							<div class="diff-file-name gt-df gt-ac gt-gap-2 gt-fw">
-								<button class="fold-file btn interact-bg gt-p-2{{if not $isExpandable}} gt-invisible{{end}}">
+					<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} tw-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
+						<h4 class="diff-file-header sticky-2nd-row ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between tw-flex-wrap">
+							<div class="diff-file-name tw-flex tw-flex-1 tw-items-center tw-gap-1 tw-flex-wrap">
+								<button class="fold-file btn interact-bg tw-p-1{{if not $isExpandable}} tw-invisible{{end}}">
 									{{if $file.ShouldBeHidden}}
 										{{svg "octicon-chevron-right" 18}}
 									{{else}}
 										{{svg "octicon-chevron-down" 18}}
 									{{end}}
 								</button>
-								<div class="gt-font-semibold gt-df gt-ac gt-mono">
+								<div class="tw-font-semibold tw-flex tw-items-center tw-font-mono">
 									{{if $file.IsBin}}
-										<span class="gt-ml-1 gt-mr-3">
+										<span class="tw-ml-0.5 tw-mr-2">
 											{{ctx.Locale.Tr "repo.diff.bin"}}
 										</span>
 									{{else}}
 										{{template "repo/diff/stats" dict "file" . "root" $}}
 									{{end}}
 								</div>
-								<span class="file gt-mono"><a class="muted file-link" title="{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}" href="#diff-{{$file.NameHash}}">{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}</a>{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}</span>
-								<button class="btn interact-fg gt-p-3" data-clipboard-text="{{$file.Name}}">{{svg "octicon-copy" 14}}</button>
-								{{if $file.IsGenerated}}
-									<span class="ui label">{{ctx.Locale.Tr "repo.diff.generated"}}</span>
-								{{end}}
-								{{if $file.IsVendored}}
-									<span class="ui label">{{ctx.Locale.Tr "repo.diff.vendored"}}</span>
-								{{end}}
-								{{if and $file.Mode $file.OldMode}}
-									{{$old := ctx.Locale.Tr ($file.ModeTranslationKey $file.OldMode)}}
-									{{$new := ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}
-									<span class="gt-ml-4 gt-mono">{{ctx.Locale.Tr "git.filemode.changed_filemode" $old $new}}</span>
-								{{else if $file.Mode}}
-									<span class="gt-ml-4 gt-mono">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span>
-								{{end}}
+								<span class="file tw-flex tw-items-center tw-font-mono tw-flex-1"><a class="muted file-link" title="{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}" href="#diff-{{$file.NameHash}}">{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}</a>
+									{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}
+									<button class="btn interact-fg tw-p-2" data-clipboard-text="{{$file.Name}}">{{svg "octicon-copy" 14}}</button>
+									{{if $file.IsGenerated}}
+										<span class="ui label">{{ctx.Locale.Tr "repo.diff.generated"}}</span>
+									{{end}}
+									{{if $file.IsVendored}}
+										<span class="ui label">{{ctx.Locale.Tr "repo.diff.vendored"}}</span>
+									{{end}}
+									{{if and $file.Mode $file.OldMode}}
+										{{$old := ctx.Locale.Tr ($file.ModeTranslationKey $file.OldMode)}}
+										{{$new := ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}
+										<span class="tw-mx-2 tw-font-mono tw-whitespace-nowrap">{{ctx.Locale.Tr "git.filemode.changed_filemode" $old $new}}</span>
+									{{else if $file.Mode}}
+										<span class="tw-mx-2 tw-font-mono tw-whitespace-nowrap">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span>
+									{{end}}
+								</span>
 							</div>
-							<div class="diff-file-header-actions gt-df gt-ac gt-gap-2 gt-fw">
+							<div class="diff-file-header-actions tw-flex tw-items-center tw-gap-1 tw-flex-wrap">
 								{{if $showFileViewToggle}}
 									<div class="ui compact icon buttons">
 										<button class="ui tiny basic button file-view-toggle" data-toggle-selector="#diff-source-{{$file.NameHash}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code"}}</button>
@@ -159,13 +161,16 @@
 								{{end}}
 								{{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}}
 									<button class="ui basic tiny button unescape-button not-mobile">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
-									<button class="ui basic tiny button escape-button gt-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
+									<button class="ui basic tiny button escape-button tw-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
 								{{end}}
 								{{if and (not $file.IsSubmodule) (not $.PageIsWiki)}}
 									{{if $file.IsDeleted}}
 										<a class="ui basic tiny button" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
 									{{else}}
 										<a class="ui basic tiny button" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
+										{{if and $.Repository.CanEnableEditor $.CanEditFile (not $file.IsLFSFile) (not $file.IsBin)}}
+											<a class="ui basic tiny button" rel="nofollow" href="{{$.HeadRepoLink}}/_edit/{{PathEscapeSegments $.HeadBranchName}}/{{PathEscapeSegments $file.Name}}?return_uri={{print $.BackToLink "#diff-" $file.NameHash | QueryEscape}}">{{ctx.Locale.Tr "repo.editor.edit_this_file"}}</a>
+										{{end}}
 									{{end}}
 								{{end}}
 								{{if $isReviewFile}}
@@ -176,15 +181,15 @@
 							</div>
 						</h4>
 						<div class="diff-file-body ui attached unstackable table segment" {{if and $file.IsViewed $.IsShowingAllCommits}}data-folded="true"{{end}}>
-							<div id="diff-source-{{$file.NameHash}}" class="file-body file-code unicode-escaped code-diff{{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}{{if $showFileViewToggle}} gt-hidden{{end}}">
+							<div id="diff-source-{{$file.NameHash}}" class="file-body file-code unicode-escaped code-diff{{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}{{if $showFileViewToggle}} tw-hidden{{end}}">
 								{{if or $file.IsIncomplete $file.IsBin}}
-									<div class="diff-file-body binary" style="padding: 5px 10px;">
+									<div class="diff-file-body binary">
 										{{if $file.IsIncomplete}}
 											{{if $file.IsIncompleteLineTooLong}}
 												{{ctx.Locale.Tr "repo.diff.file_suppressed_line_too_long"}}
 											{{else}}
 												{{ctx.Locale.Tr "repo.diff.file_suppressed"}}
-												<a class="ui basic tiny button diff-load-button" data-href="{{$.Link}}?file-only=true&files={{$file.Name}}&files={{$file.OldName}}">{{ctx.Locale.Tr "repo.diff.load"}}</a>
+												<a class="ui basic tiny button diff-load-button" data-href="?file-only=true&files={{$file.Name}}&files={{$file.OldName}}">{{ctx.Locale.Tr "repo.diff.load"}}</a>
 											{{end}}
 										{{else}}
 											{{ctx.Locale.Tr "repo.diff.bin_not_shown"}}
@@ -202,8 +207,8 @@
 							</div>
 							{{if $showFileViewToggle}}
 								{{/* for image or CSV, it can have a horizontal scroll bar, there won't be review comment context menu (position absolute) which would be clipped by "overflow" */}}
-								<div id="diff-rendered-{{$file.NameHash}}" class="file-body file-code {{if $.IsSplitStyle}}code-diff-split{{else}}code-diff-unified{{end}} gt-overflow-x-scroll">
-									<table class="chroma gt-w-full">
+								<div id="diff-rendered-{{$file.NameHash}}" class="file-body file-code {{if $.IsSplitStyle}}code-diff-split{{else}}code-diff-unified{{end}} tw-overflow-x-scroll">
+									<table class="chroma tw-w-full">
 										{{if $isImage}}
 											{{template "repo/diff/image_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead "sniffedTypeBase" $sniffedTypeBase "sniffedTypeHead" $sniffedTypeHead}}
 										{{else}}
@@ -217,10 +222,10 @@
 				{{end}}
 
 				{{if .Diff.IsIncomplete}}
-					<div class="diff-file-box diff-box file-content gt-mt-3" id="diff-incomplete">
-						<h4 class="ui top attached normal header gt-df gt-ac gt-sb">
+					<div class="diff-file-box diff-box file-content tw-mt-2" id="diff-incomplete">
+						<h4 class="ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between">
 							{{ctx.Locale.Tr "repo.diff.too_many_files"}}
-							<a class="ui basic tiny button" id="diff-show-more-files" data-href="{{$.Link}}?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
+							<a class="ui basic tiny button" id="diff-show-more-files" data-href="?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
 						</h4>
 					</div>
 				{{end}}
@@ -237,6 +242,11 @@
 					"TextareaName" "content"
 					"DropzoneParentContainer" ".ui.form"
 				)}}
+				{{if .IsAttachmentEnabled}}
+					<div class="field">
+						{{template "repo/upload" .}}
+					</div>
+				{{end}}
 				<div class="text right edit buttons">
 					<button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
 					<button class="ui primary save button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
@@ -246,5 +256,6 @@
 	{{end}}
 	{{if (not .DiffNotAvailable)}}
 		{{template "repo/issue/view_content/reference_issue_dialog" .}}
+		{{template "shared/user/block_user_dialog" .}}
 	{{end}}
 </div>
diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl
index 767c2613a0..856b3da01a 100644
--- a/templates/repo/diff/comment_form.tmpl
+++ b/templates/repo/diff/comment_form.tmpl
@@ -1,5 +1,5 @@
 {{if and $.root.SignedUserID (not $.Repository.IsArchived)}}
-	<form class="ui form {{if $.hidden}}gt-hidden comment-form{{end}}" action="{{$.root.Issue.Link}}/files/reviews/comments" method="post">
+	<form class="ui form {{if $.hidden}}tw-hidden comment-form{{end}}" action="{{$.root.Issue.Link}}/files/reviews/comments" method="post">
 	{{$.root.CsrfTokenHtml}}
 		<input type="hidden" name="origin" value="{{if $.root.PageIsPullFiles}}diff{{else}}timeline{{end}}">
 		<input type="hidden" name="latest_commit_id" value="{{$.root.AfterCommitID}}">
@@ -19,9 +19,15 @@
 			"DisableAutosize" "true"
 		)}}
 
-		<div class="field footer gt-mx-3">
-			<span class="markup-info">{{svg "octicon-markup"}} {{ctx.Locale.Tr "repo.diff.comment.markdown_info"}}</span>
-			<div class="gt-text-right">
+		{{if $.root.IsAttachmentEnabled}}
+			<div class="field">
+				{{template "repo/upload" $.root}}
+			</div>
+		{{end}}
+
+		<div class="field footer tw-mx-2">
+			<span class="markup-info">{{svg "octicon-markdown"}} {{ctx.Locale.Tr "repo.diff.comment.markdown_info"}}</span>
+			<div class="tw-text-right">
 				{{if $.reply}}
 					<button class="ui submit primary tiny button btn-reply" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button>
 					<input type="hidden" name="reply" value="{{$.reply}}">
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index 2fbfe2fd6a..a9120465bd 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -8,36 +8,36 @@
 		{{template "shared/user/avatarlink" dict "user" .Poster}}
 	{{end}}
 	<div class="content comment-container">
-		<div class="ui top attached header comment-header gt-df gt-ac gt-sb">
-			<div class="comment-header-left gt-df gt-ac">
+		<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between">
+			<div class="comment-header-left tw-flex tw-items-center">
 				{{if .OriginalAuthor}}
-					<span class="text black gt-font-semibold gt-mr-2">
+					<span class="text black tw-font-semibold tw-mr-1">
 						{{svg (MigrationIcon $.root.Repository.GetOriginalURLHostname)}}
 						{{.OriginalAuthor}}
 					</span>
 					<span class="text grey muted-links">
-						{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}}
 					</span>
 					<span class="text migrate">
 						{{if $.root.Repository.OriginalURL}}
-							({{ctx.Locale.Tr "repo.migrated_from" ($.root.Repository.OriginalURL | Escape) ($.root.Repository.GetOriginalURLHostname | Escape) | Safe}})
+							({{ctx.Locale.Tr "repo.migrated_from" $.root.Repository.OriginalURL $.root.Repository.GetOriginalURLHostname}})
 						{{end}}
 					</span>
 				{{else}}
 					<span class="text grey muted-links">
 						{{template "shared/user/namelink" .Poster}}
-						{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}}
 					</span>
 				{{end}}
 			</div>
-			<div class="comment-header-right actions gt-df gt-ac">
+			<div class="comment-header-right actions tw-flex tw-items-center">
 				{{if .Invalidated}}
 					{{$referenceUrl := printf "%s#%s" $.root.Issue.Link .HashTag}}
-					<a href="{{AppSubUrl}}{{$referenceUrl}}" class="ui label basic small" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
+					<a href="{{$referenceUrl}}" class="ui label basic small" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
 						{{ctx.Locale.Tr "repo.issues.review.outdated"}}
 					</a>
 				{{end}}
-				{{if and .Review}}
+				{{if .Review}}
 					{{if eq .Review.Type 0}}
 						<div class="ui label basic small yellow pending-label" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.pending.tooltip" (ctx.Locale.Tr "repo.diff.review") (ctx.Locale.Tr "repo.diff.review.approve") (ctx.Locale.Tr "repo.diff.review.comment") (ctx.Locale.Tr "repo.diff.review.reject")}}">
 						{{ctx.Locale.Tr "repo.issues.review.pending"}}
@@ -55,13 +55,16 @@
 		<div class="ui attached segment comment-body">
 			<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}data-can-edit="true"{{end}}>
 			{{if .RenderedContent}}
-				{{.RenderedContent|Str2html}}
+				{{.RenderedContent}}
 			{{else}}
 				<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 			{{end}}
 			</div>
-			<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
-			<div class="edit-content-zone gt-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-context="{{$.root.RepoLink}}"></div>
+			<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
+			<div class="edit-content-zone tw-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-context="{{$.root.RepoLink}}" data-attachment-url="{{$.root.RepoLink}}/comments/{{.ID}}/attachments"></div>
+			{{if .Attachments}}
+				{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
+			{{end}}
 		</div>
 		{{$reactions := .Reactions.GroupByType}}
 		{{if $reactions}}
diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl
index 15574ad988..d0472577d0 100644
--- a/templates/repo/diff/compare.tmpl
+++ b/templates/repo/diff/compare.tmpl
@@ -11,14 +11,6 @@
 			{{ctx.Locale.Tr "action.compare_commits_general"}}
 		{{end}}
 	</h2>
-	{{if .Flash.WarningMsg}}
-		{{/*
-			There's already an importing of alert.tmpl in new_form.tmpl,
-			but only the negative message will be displayed within forms for some reasons, see semantic.css:10659.
-			To avoid repeated negative messages, the importing here if for .Flash.WarningMsg only.
-		*/}}
-		{{template "base/alert" .}}
-	{{end}}
 	{{$BaseCompareName := $.BaseName -}}
 	{{- $HeadCompareName := $.HeadRepo.OwnerName -}}
 	{{- if and (eq $.BaseName $.HeadRepo.OwnerName) (ne $.Repository.Name $.HeadRepo.Name) -}}
@@ -36,8 +28,8 @@
 		{{- end -}}
 	{{- end -}}
 	<div class="ui segment choose branch">
-		<a class="gt-mr-3" href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments $.HeadBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{end}}{{PathEscapeSegments $.BaseBranch}}" title="{{ctx.Locale.Tr "repo.pulls.switch_head_and_base"}}">{{svg "octicon-git-compare"}}</a>
-		<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "repo.pulls.no_results"}}">
+		<a class="tw-mr-2" href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments $.HeadBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{end}}{{PathEscapeSegments $.BaseBranch}}" title="{{ctx.Locale.Tr "repo.pulls.switch_head_and_base"}}">{{svg "octicon-git-compare"}}</a>
+		<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}">
 			<div class="ui basic small button">
 				<span class="text">{{if $.PageIsComparePull}}{{ctx.Locale.Tr "repo.pulls.compare_base"}}{{else}}{{ctx.Locale.Tr "repo.compare.compare_base"}}{{end}}: {{$BaseCompareName}}:{{$.BaseBranch}}</span>
 				{{svg "octicon-triangle-down" 14 "dropdown icon"}}
@@ -52,12 +44,12 @@
 						<div class="two column row">
 							<a class="reference column" href="#" data-target=".base-branch-list">
 								<span class="text black">
-									{{svg "octicon-git-branch" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.branches"}}
+									{{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}}
 								</span>
 							</a>
 							<a class="reference column" href="#" data-target=".base-tag-list">
 								<span class="text black">
-									{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.tags"}}
+									{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}}
 								</span>
 							</a>
 						</div>
@@ -83,7 +75,7 @@
 						{{end}}
 					{{end}}
 				</div>
-				<div class="scrolling menu reference-list-menu base-tag-list gt-hidden">
+				<div class="scrolling menu reference-list-menu base-tag-list tw-hidden">
 					{{range .Tags}}
 						<div class="item {{if eq $.BaseBranch .}}selected{{end}}" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments $.HeadBranch}}">{{$BaseCompareName}}:{{.}}</div>
 					{{end}}
@@ -121,12 +113,12 @@
 						<div class="two column row">
 							<a class="reference column" href="#" data-target=".head-branch-list">
 								<span class="text black">
-									{{svg "octicon-git-branch" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.branches"}}
+									{{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}}
 								</span>
 							</a>
 							<a class="reference column" href="#" data-target=".head-tag-list">
 								<span class="text black">
-									{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.tags"}}
+									{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}}
 								</span>
 							</a>
 						</div>
@@ -152,7 +144,7 @@
 						{{end}}
 					{{end}}
 				</div>
-				<div class="scrolling menu reference-list-menu head-tag-list gt-hidden">
+				<div class="scrolling menu reference-list-menu head-tag-list tw-hidden">
 					{{range .HeadTags}}
 						<div class="{{if eq $.HeadBranch .}}selected{{end}} item" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments .}}">{{$HeadCompareName}}:{{.}}</div>
 					{{end}}
@@ -179,10 +171,10 @@
 	{{if .IsNothingToCompare}}
 		{{if and $.IsSigned $.AllowEmptyPr (not .Repository.IsArchived) .PageIsComparePull}}
 			<div class="ui segment">{{ctx.Locale.Tr "repo.pulls.nothing_to_compare_and_allow_empty_pr"}}</div>
-			<div class="ui info message show-form-container {{if .Flash}}gt-hidden{{end}}">
+			<div class="ui info message show-form-container {{if .Flash}}tw-hidden{{end}}">
 				<button class="ui button primary show-form">{{ctx.Locale.Tr "repo.pulls.new"}}</button>
 			</div>
-			<div class="pullrequest-form {{if not .Flash}}gt-hidden{{end}}">
+			<div class="pullrequest-form {{if not .Flash}}tw-hidden{{end}}">
 				{{template "repo/issue/new_form" .}}
 			</div>
 		{{else if and .HeadIsBranch .BaseIsBranch}}
@@ -194,7 +186,7 @@
 		{{if .HasPullRequest}}
 			<div class="ui segment grid title">
 				<div class="twelve wide column issue-title">
-					{{ctx.Locale.Tr "repo.pulls.has_pull_request" (print (Escape $.RepoLink) "/pulls/" .PullRequest.Issue.Index) (Escape $.RepoRelPath) .PullRequest.Index | Safe}}
+					{{ctx.Locale.Tr "repo.pulls.has_pull_request" (print $.RepoLink "/pulls/" .PullRequest.Issue.Index) $.RepoRelPath .PullRequest.Index}}
 					<h1>
 						<span id="issue-title">{{RenderIssueTitle $.Context .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}}</span>
 						<span class="index">#{{.PullRequest.Issue.Index}}</span>
@@ -202,30 +194,30 @@
 				</div>
 				<div class="four wide column middle aligned text right">
 				{{- if .PullRequest.HasMerged -}}
-				<a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button purple show-form">{{svg "octicon-git-merge" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a>
+				<a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button purple show-form">{{svg "octicon-git-merge" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a>
 				{{else if .Issue.IsClosed}}
-				<a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button red show-form">{{svg "octicon-issue-closed" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a>
+				<a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button red show-form">{{svg "octicon-issue-closed" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a>
 				{{else}}
-				<a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button primary show-form">{{svg "octicon-git-pull-request" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a>
+				<a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button primary show-form">{{svg "octicon-git-pull-request" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a>
 				{{end}}
 				</div>
 			</div>
 		{{else}}
 			{{if and $.IsSigned (not .Repository.IsArchived)}}
-				<div class="ui info message show-form-container {{if .Flash}}gt-hidden{{end}}">
+				<div class="ui info message show-form-container {{if .Flash}}tw-hidden{{end}}">
 					<button class="ui button primary show-form">{{ctx.Locale.Tr "repo.pulls.new"}}</button>
 				</div>
 			{{else if .Repository.IsArchived}}
-				<div class="ui warning message gt-text-center">
+				<div class="ui warning message tw-text-center">
 					{{if .Repository.ArchivedUnix.IsZero}}
 						{{ctx.Locale.Tr "repo.archive.title"}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix) | Safe}}
+						{{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix)}}
 					{{end}}
 				</div>
 			{{end}}
 			{{if $.IsSigned}}
-				<div class="pullrequest-form {{if not .Flash}}gt-hidden{{end}}">
+				<div class="pullrequest-form {{if not .Flash}}tw-hidden{{end}}">
 					{{template "repo/issue/new_form" .}}
 				</div>
 			{{end}}
diff --git a/templates/repo/diff/conversation.tmpl b/templates/repo/diff/conversation.tmpl
index feca7b6c0b..c263ddcdd6 100644
--- a/templates/repo/diff/conversation.tmpl
+++ b/templates/repo/diff/conversation.tmpl
@@ -1,66 +1,72 @@
-{{$resolved := (index .comments 0).IsResolved}}
-{{$invalid := (index .comments 0).Invalidated}}
-{{$resolveDoer := (index .comments 0).ResolveDoer}}
-{{$isNotPending := (not (eq (index .comments 0).Review.Type 0))}}
-{{$referenceUrl := printf "%s#%s" $.Issue.Link (index .comments 0).HashTag}}
-<div class="conversation-holder" data-path="{{(index .comments 0).TreePath}}" data-side="{{if lt (index .comments 0).Line 0}}left{{else}}right{{end}}" data-idx="{{(index .comments 0).UnsignedLine}}">
-	{{if $resolved}}
-		<div class="ui attached header resolved-placeholder gt-df gt-ac gt-sb">
-			<div class="ui grey text gt-df gt-ac gt-fw gt-gap-2">
-				{{svg "octicon-check" 16 "icon gt-mr-2"}}
-				<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
-				{{if $invalid}}
-					<!--
-					We only handle the case $resolved=true and $invalid=true in this template because if the comment is not resolved it has the outdated label in the comments area (not the header above).
-					The case $resolved=false and $invalid=true is handled in repo/diff/comments.tmpl
-					-->
-					<a href="{{AppSubUrl}}{{$referenceUrl}}" class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
-						{{ctx.Locale.Tr "repo.issues.review.outdated"}}
-					</a>
+{{if len .comments}}
+	{{$comment := index .comments 0}}
+	{{$resolved := $comment.IsResolved}}
+	{{$invalid := $comment.Invalidated}}
+	{{$resolveDoer := $comment.ResolveDoer}}
+	{{$hasReview := and $comment.Review}}
+	{{$isReviewPending := and $hasReview (eq $comment.Review.Type 0)}}
+	{{$referenceUrl := printf "%s#%s" $.Issue.Link $comment.HashTag}}
+	<div class="conversation-holder" data-path="{{$comment.TreePath}}" data-side="{{if lt $comment.Line 0}}left{{else}}right{{end}}" data-idx="{{$comment.UnsignedLine}}">
+		{{if $resolved}}
+			<div class="ui attached header resolved-placeholder tw-flex tw-items-center tw-justify-between">
+				<div class="ui grey text tw-flex tw-items-center tw-flex-wrap tw-gap-1">
+					{{svg "octicon-check" 16 "icon tw-mr-1"}}
+					<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
+					{{if $invalid}}
+						<!--
+						We only handle the case $resolved=true and $invalid=true in this template because if the comment is not resolved it has the outdated label in the comments area (not the header above).
+						The case $resolved=false and $invalid=true is handled in repo/diff/comments.tmpl
+						-->
+						<a href="{{$referenceUrl}}" class="ui label basic small tw-ml-2" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
+							{{ctx.Locale.Tr "repo.issues.review.outdated"}}
+						</a>
+					{{end}}
+				</div>
+				<div class="tw-flex tw-items-center tw-gap-2">
+					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button show-outdated tw-flex tw-items-center">
+						{{svg "octicon-unfold" 16 "tw-mr-2"}}
+						{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
+					</button>
+					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button hide-outdated tw-flex tw-items-center tw-hidden">
+						{{svg "octicon-fold" 16 "tw-mr-2"}}
+						{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
+					</button>
+				</div>
+			</div>
+		{{end}}
+		<div id="code-comments-{{$comment.ID}}" class="field comment-code-cloud {{if $resolved}}tw-hidden{{end}}">
+			<div class="comment-list">
+				<ui class="ui comments">
+					{{template "repo/diff/comments" dict "root" $ "comments" .comments}}
+				</ui>
+			</div>
+			<div class="tw-flex tw-justify-end tw-items-center tw-flex-wrap tw-mt-2">
+				<div class="ui buttons tw-mr-1">
+					<button class="ui icon tiny basic button previous-conversation">
+						{{svg "octicon-arrow-up" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.previous"}}
+					</button>
+					<button class="ui icon tiny basic button next-conversation">
+						{{svg "octicon-arrow-down" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.next"}}
+					</button>
+				</div>
+				{{if and $.CanMarkConversation $hasReview (not $isReviewPending)}}
+					<button class="ui icon tiny basic button resolve-conversation" data-origin="diff" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{$comment.ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation">
+						{{if $resolved}}
+							{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
+						{{else}}
+							{{ctx.Locale.Tr "repo.issues.review.resolve_conversation"}}
+						{{end}}
+					</button>
+				{{end}}
+				{{if and $.SignedUserID (not $.Repository.IsArchived)}}
+					<button class="comment-form-reply ui primary tiny labeled icon button tw-ml-1 tw-mr-0">
+						{{svg "octicon-reply" 16 "reply icon tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
+					</button>
 				{{end}}
 			</div>
-			<div class="gt-df gt-ac gt-gap-3">
-				<button id="show-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="ui tiny labeled button show-outdated gt-df gt-ac">
-					{{svg "octicon-unfold" 16 "gt-mr-3"}}
-					{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
-				</button>
-				<button id="hide-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="ui tiny labeled button hide-outdated gt-df gt-ac gt-hidden">
-					{{svg "octicon-fold" 16 "gt-mr-3"}}
-					{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
-				</button>
-			</div>
+			{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" $comment.ReviewID "root" $ "comment" $comment}}
 		</div>
-	{{end}}
-	<div id="code-comments-{{(index  .comments 0).ID}}" class="field comment-code-cloud {{if $resolved}}gt-hidden{{end}}">
-		<div class="comment-list">
-			<ui class="ui comments">
-				{{template "repo/diff/comments" dict "root" $ "comments" .comments}}
-			</ui>
-		</div>
-		<div class="gt-df gt-je gt-ac gt-fw gt-mt-3">
-			<div class="ui buttons gt-mr-2">
-				<button class="ui icon tiny basic button previous-conversation">
-					{{svg "octicon-arrow-up" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.previous"}}
-				</button>
-				<button class="ui icon tiny basic button next-conversation">
-					{{svg "octicon-arrow-down" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.next"}}
-				</button>
-			</div>
-			{{if and $.CanMarkConversation $isNotPending}}
-				<button class="ui icon tiny basic button resolve-conversation" data-origin="diff" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{(index .comments 0).ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation">
-					{{if $resolved}}
-						{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
-					{{else}}
-						{{ctx.Locale.Tr "repo.issues.review.resolve_conversation"}}
-					{{end}}
-				</button>
-			{{end}}
-			{{if and $.SignedUserID (not $.Repository.IsArchived)}}
-				<button class="comment-form-reply ui primary tiny labeled icon button gt-ml-2 gt-mr-0">
-					{{svg "octicon-reply" 16 "reply icon gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
-				</button>
-			{{end}}
-		</div>
-		{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" (index .comments 0).ReviewID "root" $ "comment" (index .comments 0)}}
 	</div>
-</div>
+{{else}}
+	{{template "repo/diff/conversation_outdated"}}
+{{end}}
diff --git a/templates/repo/diff/image_diff.tmpl b/templates/repo/diff/image_diff.tmpl
index 02cca784f6..9ad7916398 100644
--- a/templates/repo/diff/image_diff.tmpl
+++ b/templates/repo/diff/image_diff.tmpl
@@ -7,15 +7,15 @@
 			data-mime-before="{{.sniffedTypeBase.GetMimeType}}"
 			data-mime-after="{{.sniffedTypeHead.GetMimeType}}"
 		>
-			<div class="ui secondary pointing tabular top attached borderless menu new-menu">
-				<div class="new-menu-inner">
+			<overflow-menu class="ui secondary pointing tabular top attached borderless menu">
+				<div class="overflow-menu-items tw-justify-center">
 					<a class="item active" data-tab="diff-side-by-side-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.side_by_side"}}</a>
 					{{if and .blobBase .blobHead}}
 					<a class="item" data-tab="diff-swipe-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.swipe"}}</a>
 					<a class="item" data-tab="diff-overlay-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.overlay"}}</a>
 					{{end}}
 				</div>
-			</div>
+			</overflow-menu>
 			<div class="image-diff-tabs is-loading">
 				<div class="ui bottom attached tab image-diff-container active" data-tab="diff-side-by-side-{{.file.Index}}">
 					<div class="diff-side-by-side">
diff --git a/templates/repo/diff/new_review.tmpl b/templates/repo/diff/new_review.tmpl
index ae7182c930..a2eae007a5 100644
--- a/templates/repo/diff/new_review.tmpl
+++ b/templates/repo/diff/new_review.tmpl
@@ -1,5 +1,5 @@
 <div id="review-box">
-	<button class="ui tiny primary button gt-pr-2 gt-df js-btn-review {{if not $.IsShowingAllCommits}}disabled{{end}}" {{if not $.IsShowingAllCommits}}data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff"}}"{{end}}>
+	<button class="ui tiny primary button tw-pr-1 tw-flex js-btn-review {{if not $.IsShowingAllCommits}}disabled{{end}}" {{if not $.IsShowingAllCommits}}data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff"}}"{{end}}>
 		{{ctx.Locale.Tr "repo.diff.review"}}
 		<span class="ui small label review-comments-counter" data-pending-comment-number="{{.PendingCodeCommentNumber}}">{{.PendingCodeCommentNumber}}</span>
 		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
@@ -10,8 +10,8 @@
 			<form class="ui form form-fetch-action" action="{{.Link}}/reviews/submit" method="post">
 				{{.CsrfTokenHtml}}
 				<input type="hidden" name="commit_id" value="{{.AfterCommitID}}">
-				<div class="field gt-df gt-ac">
-					<div class="gt-f1">{{ctx.Locale.Tr "repo.diff.review.header"}}</div>
+				<div class="field tw-flex tw-items-center">
+					<div class="tw-flex-1">{{ctx.Locale.Tr "repo.diff.review.header"}}</div>
 					<a class="muted close">{{svg "octicon-x" 16}}</a>
 				</div>
 				<div class="field">
@@ -31,7 +31,7 @@
 				<div class="divider"></div>
 				{{$showSelfTooltip := (and $.IsSigned ($.Issue.IsPoster $.SignedUser.ID))}}
 				{{if $showSelfTooltip}}
-					<span class="gt-dib" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
+					<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
 						<button type="submit" name="type" value="approve" disabled class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
 					</span>
 				{{else}}
@@ -39,7 +39,7 @@
 				{{end}}
 				<button type="submit" name="type" value="comment" class="ui submit tiny basic button btn-submit">{{ctx.Locale.Tr "repo.diff.review.comment"}}</button>
 				{{if $showSelfTooltip}}
-					<span class="gt-dib" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
+					<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
 						<button type="submit" name="type" value="reject" disabled class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
 					</span>
 				{{else}}
diff --git a/templates/repo/diff/options_dropdown.tmpl b/templates/repo/diff/options_dropdown.tmpl
index 3bcb877cc6..09b7b80e41 100644
--- a/templates/repo/diff/options_dropdown.tmpl
+++ b/templates/repo/diff/options_dropdown.tmpl
@@ -13,17 +13,17 @@
 			<a class="item" href="{{$.RepoLink}}/commit/{{PathEscape .Commit.ID.String}}.diff" download="{{ShortSha .Commit.ID.String}}.diff">{{ctx.Locale.Tr "repo.diff.download_diff"}}</a>
 		{{end}}
 		<a id="expand-files-btn" class="item">{{ctx.Locale.Tr "repo.pulls.expand_files"}}</a>
-		<a id="collapse-files-btn"class="item">{{ctx.Locale.Tr "repo.pulls.collapse_files"}}</a>
+		<a id="collapse-files-btn" class="item">{{ctx.Locale.Tr "repo.pulls.collapse_files"}}</a>
 		{{if .Issue.Index}}
 			{{if .ShowOutdatedComments}}
 				<a class="item" href="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated=false">
-					<label class="gt-pointer-events-none">
+					<label class="tw-pointer-events-none">
 						{{ctx.Locale.Tr "repo.issues.review.option.hide_outdated_comments"}}
 					</label>
 				</a>
 			{{else}}
 				<a class="item" href="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated=true">
-					<label class="gt-pointer-events-none">
+					<label class="tw-pointer-events-none">
 						{{ctx.Locale.Tr "repo.issues.review.option.show_outdated_comments"}}
 					</label>
 				</a>
diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl
index 5b0d982e96..67e2b195de 100644
--- a/templates/repo/diff/section_split.tmpl
+++ b/templates/repo/diff/section_split.tmpl
@@ -16,19 +16,19 @@
 			<tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}" data-line-type="{{.GetHTMLDiffLineType}}">
 				{{if eq .GetType 4}}
 					<td class="lines-num lines-num-old">
-						<div class="gt-df">
+						<div class="tw-flex">
 						{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
-							<button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
+							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 								{{svg "octicon-fold-down"}}
 							</button>
 						{{end}}
 						{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}}
-							<button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
+							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 								{{svg "octicon-fold-up"}}
 							</button>
 						{{end}}
 						{{if eq $line.GetExpandDirection 2}}
-							<button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
+							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 								{{svg "octicon-fold"}}
 							</button>
 						{{end}}
@@ -44,10 +44,10 @@
 					{{- $rightDiff := ""}}{{if $match.RightIdx}}{{$rightDiff = $section.GetComputedInlineDiffFor $match ctx.Locale}}{{end}}
 					<td class="lines-num lines-num-old del-code" data-line-num="{{$line.LeftIdx}}"><span rel="diff-{{$file.NameHash}}L{{$line.LeftIdx}}"></span></td>
 					<td class="lines-escape del-code lines-escape-old">{{if $line.LeftIdx}}{{if $leftDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $leftDiff}}"></button>{{end}}{{end}}</td>
-					<td class="lines-type-marker lines-type-marker-old del-code"><span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
+					<td class="lines-type-marker lines-type-marker-old del-code"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
 					<td class="lines-code lines-code-old del-code">{{/*
 						*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/*
-							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} gt-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/*
+							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/*
 								*/}}{{svg "octicon-plus"}}{{/*
 							*/}}</button>{{/*
 						*/}}{{end}}{{/*
@@ -59,10 +59,10 @@
 					*/}}</td>
 					<td class="lines-num lines-num-new add-code" data-line-num="{{if $match.RightIdx}}{{$match.RightIdx}}{{end}}"><span rel="{{if $match.RightIdx}}diff-{{$file.NameHash}}R{{$match.RightIdx}}{{end}}"></span></td>
 					<td class="lines-escape add-code lines-escape-new">{{if $match.RightIdx}}{{if $rightDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $rightDiff}}"></button>{{end}}{{end}}</td>
-					<td class="lines-type-marker lines-type-marker-new add-code">{{if $match.RightIdx}}<span class="gt-mono" data-type-marker="{{$match.GetLineTypeMarker}}"></span>{{end}}</td>
+					<td class="lines-type-marker lines-type-marker-new add-code">{{if $match.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$match.GetLineTypeMarker}}"></span>{{end}}</td>
 					<td class="lines-code lines-code-new add-code">{{/*
 						*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/*
-							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $match.CanComment)}} gt-invisible{{end}}" data-side="right" data-idx="{{$match.RightIdx}}">{{/*
+							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $match.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$match.RightIdx}}">{{/*
 								*/}}{{svg "octicon-plus"}}{{/*
 							*/}}</button>{{/*
 						*/}}{{end}}{{/*
@@ -76,10 +76,10 @@
 					{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}}
 					<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$file.NameHash}}L{{$line.LeftIdx}}{{end}}"></span></td>
 					<td class="lines-escape lines-escape-old">{{if $line.LeftIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}{{end}}</td>
-					<td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
+					<td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
 					<td class="lines-code lines-code-old">{{/*
 						*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2))}}{{/*
-							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} gt-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/*
+							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/*
 								*/}}{{svg "octicon-plus"}}{{/*
 							*/}}</button>{{/*
 						*/}}{{end}}{{/*
@@ -91,10 +91,10 @@
 					*/}}</td>
 					<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$file.NameHash}}R{{$line.RightIdx}}{{end}}"></span></td>
 					<td class="lines-escape lines-escape-new">{{if $line.RightIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}{{end}}</td>
-					<td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
+					<td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
 					<td class="lines-code lines-code-new">{{/*
 						*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3))}}{{/*
-							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $line.CanComment)}} gt-invisible{{end}}" data-side="right" data-idx="{{$line.RightIdx}}">{{/*
+							*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$line.RightIdx}}">{{/*
 								*/}}{{svg "octicon-plus"}}{{/*
 							*/}}</button>{{/*
 						*/}}{{end}}{{/*
diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl
index 2b901411e2..4111159709 100644
--- a/templates/repo/diff/section_unified.tmpl
+++ b/templates/repo/diff/section_unified.tmpl
@@ -12,19 +12,19 @@
 			{{if eq .GetType 4}}
 				{{if $.root.AfterCommitID}}
 					<td colspan="2" class="lines-num">
-						<div class="gt-df">
+						<div class="tw-flex">
 							{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
-								<button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
+								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 									{{svg "octicon-fold-down"}}
 								</button>
 							{{end}}
 							{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}}
-								<button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
+								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 									{{svg "octicon-fold-up"}}
 								</button>
 							{{end}}
 							{{if eq $line.GetExpandDirection 2}}
-								<button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
+								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}">
 									{{svg "octicon-fold"}}
 								</button>
 							{{end}}
@@ -44,7 +44,7 @@
 					<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>
 				{{- end -}}
 			</td>
-			<td class="lines-type-marker"><span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
+			<td class="lines-type-marker"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
 			{{if eq .GetType 4}}
 				<td class="chroma lines-code blob-hunk">{{/*
 					*/}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/*
@@ -52,7 +52,7 @@
 			{{else}}
 				<td class="chroma lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}">{{/*
 					*/}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/*
-						*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-{{if $line.RightIdx}}right{{else}}left{{end}}{{if (not $line.CanComment)}} gt-invisible{{end}}" data-side="{{if $line.RightIdx}}right{{else}}left{{end}}" data-idx="{{if $line.RightIdx}}{{$line.RightIdx}}{{else}}{{$line.LeftIdx}}{{end}}">{{/*
+						*/}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-{{if $line.RightIdx}}right{{else}}left{{end}}{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="{{if $line.RightIdx}}right{{else}}left{{end}}" data-idx="{{if $line.RightIdx}}{{$line.RightIdx}}{{else}}{{$line.LeftIdx}}{{end}}">{{/*
 							*/}}{{svg "octicon-plus"}}{{/*
 						*/}}</button>{{/*
 					*/}}{{end}}{{/*
diff --git a/templates/repo/diff/stats.tmpl b/templates/repo/diff/stats.tmpl
index db468ab6c8..d0dff1bd09 100644
--- a/templates/repo/diff/stats.tmpl
+++ b/templates/repo/diff/stats.tmpl
@@ -1,5 +1,5 @@
 {{Eval .file.Addition "+" .file.Deletion}}
-<span class="diff-stats-bar gt-mx-3" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion | Str2html}}">
+<span class="diff-stats-bar tw-mx-2" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion}}">
 	{{/* if the denominator is zero, then the float result is "width: NaNpx", as before, it just works */}}
 	<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .file.Addition "/" "(" .file.Addition "+" .file.Deletion "+" 0.0 ")"}}%"></div>
 </span>
diff --git a/templates/repo/diff/whitespace_dropdown.tmpl b/templates/repo/diff/whitespace_dropdown.tmpl
index 7bf2ac9aec..c54de165a4 100644
--- a/templates/repo/diff/whitespace_dropdown.tmpl
+++ b/templates/repo/diff/whitespace_dropdown.tmpl
@@ -2,26 +2,26 @@
 	{{svg "gitea-whitespace"}}
 	<div class="menu">
 		<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=show-all&show-outdated={{$.ShowOutdatedComments}}">
-			<label class="gt-pointer-events-none">
-				<input class="gt-mr-3 gt-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "show-all"}} checked{{end}}>
+			<label class="tw-pointer-events-none">
+				<input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "show-all"}} checked{{end}}>
 				{{ctx.Locale.Tr "repo.diff.whitespace_show_everything"}}
 			</label>
 		</a>
 		<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-all&show-outdated={{$.ShowOutdatedComments}}">
-			<label class="gt-pointer-events-none">
-				<input class="gt-mr-3 gt-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-all"}} checked{{end}}>
+			<label class="tw-pointer-events-none">
+				<input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-all"}} checked{{end}}>
 				{{ctx.Locale.Tr "repo.diff.whitespace_ignore_all_whitespace"}}
 			</label>
 		</a>
 		<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-change&show-outdated={{$.ShowOutdatedComments}}">
-			<label class="gt-pointer-events-none">
-				<input class="gt-mr-3 gt-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-change"}} checked{{end}}>
+			<label class="tw-pointer-events-none">
+				<input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-change"}} checked{{end}}>
 				{{ctx.Locale.Tr "repo.diff.whitespace_ignore_amount_changes"}}
 			</label>
 		</a>
 		<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-eol&show-outdated={{$.ShowOutdatedComments}}">
-			<label class="gt-pointer-events-none">
-				<input class="gt-mr-3 gt-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-eol"}} checked{{end}}>
+			<label class="tw-pointer-events-none">
+				<input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-eol"}} checked{{end}}>
 				{{ctx.Locale.Tr "repo.diff.whitespace_ignore_at_eol"}}
 			</label>
 		</a>
diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl
index ab2c3c3349..f9c9eef5aa 100644
--- a/templates/repo/editor/cherry_pick.tmpl
+++ b/templates/repo/editor/cherry_pick.tmpl
@@ -11,11 +11,11 @@
 			<div class="repo-editor-header">
 				<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
 					{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}}
-					{{$shalink := printf `<a class="ui primary sha label" href="%s">%s</a>` (Escape $shaurl) (ShortSha .SHA)}}
+					{{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .SHA)}}
 					{{if eq .CherryPickType "revert"}}
-						{{ctx.Locale.Tr "repo.editor.revert" $shalink | Str2html}}
+						{{ctx.Locale.Tr "repo.editor.revert" $shalink}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.editor.cherry_pick" $shalink | Str2html}}
+						{{ctx.Locale.Tr "repo.editor.cherry_pick" $shalink}}
 					{{end}}
 					<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a>
 					<div class="breadcrumb-divider">:</div>
diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl
index 34dde576a1..21ef63288f 100644
--- a/templates/repo/editor/commit_form.tmpl
+++ b/templates/repo/editor/commit_form.tmpl
@@ -9,7 +9,7 @@
 			{{ctx.Locale.Tr "repo.editor.commit_changes"}}
 		{{- end}}</h3>
 		<div class="field">
-			<input name="commit_summary" placeholder="{{if .PageIsDelete}}{{ctx.Locale.Tr "repo.editor.delete" .TreePath}}{{else if .PageIsUpload}}{{ctx.Locale.Tr "repo.editor.upload_files_to_dir" .TreePath}}{{else if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.add_tmpl"}}{{else if .PageIsPatch}}{{ctx.Locale.Tr "repo.editor.patch"}}{{else}}{{ctx.Locale.Tr "repo.editor.update" .TreePath}}{{end}}" value="{{.commit_summary}}" autofocus>
+			<input name="commit_summary" maxlength="100" placeholder="{{if .PageIsDelete}}{{ctx.Locale.Tr "repo.editor.delete" .TreePath}}{{else if .PageIsUpload}}{{ctx.Locale.Tr "repo.editor.upload_files_to_dir" .TreePath}}{{else if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.add_tmpl"}}{{else if .PageIsPatch}}{{ctx.Locale.Tr "repo.editor.patch"}}{{else}}{{ctx.Locale.Tr "repo.editor.update" .TreePath}}{{end}}" value="{{.commit_summary}}" autofocus>
 		</div>
 		<div class="field">
 			<textarea name="commit_message" placeholder="{{ctx.Locale.Tr "repo.editor.commit_message_desc"}}" rows="5">{{.commit_message}}</textarea>
@@ -26,7 +26,7 @@
 					<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" button_text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}>
 					<label>
 						{{svg "octicon-git-commit"}}
-						{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" (.BranchName|Escape) | Safe}}
+						{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}}
 						{{if not .CanCommitToBranch.CanCommitToBranch}}
 						<div class="ui visible small warning message">
 							{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
@@ -39,36 +39,36 @@
 					</label>
 				</div>
 			</div>
-			{{if not .Repository.IsEmpty}}
-			<div class="field">
-				<div class="ui radio checkbox">
-					{{if .CanCreatePullRequest}}
-						<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" button_text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
-					{{else}}
-						<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" button_text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
-					{{end}}
-					<label>
-						{{svg "octicon-git-pull-request"}}
+			{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
+				<div class="field">
+					<div class="ui radio checkbox">
 						{{if .CanCreatePullRequest}}
-							{{ctx.Locale.Tr "repo.editor.create_new_branch" | Safe}}
+							<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" button_text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
 						{{else}}
-							{{ctx.Locale.Tr "repo.editor.create_new_branch_np" | Safe}}
+							<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" button_text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
 						{{end}}
-					</label>
+						<label>
+							{{svg "octicon-git-pull-request"}}
+							{{if .CanCreatePullRequest}}
+								{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
+							{{else}}
+								{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}
+							{{end}}
+						</label>
+					</div>
 				</div>
-			</div>
-			<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}gt-hidden{{end}}">
-				<div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}">
-					{{svg "octicon-git-branch"}}
-					<input type="text" name="new_branch_name" value="{{.new_branch_name}}" class="input-contrast gt-mr-2 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
-					<span class="text-muted js-quick-pull-normalization-info"></span>
+				<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}tw-hidden{{end}}">
+					<div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}">
+						{{svg "octicon-git-branch"}}
+						<input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast tw-mr-1 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
+						<span class="text-muted js-quick-pull-normalization-info"></span>
+					</div>
 				</div>
-			</div>
 			{{end}}
 		</div>
 	</div>
 	<button id="commit-button" type="submit" class="ui primary button">
 		{{if eq .commit_choice "commit-to-new-branch"}}{{ctx.Locale.Tr "repo.editor.propose_file_change"}}{{else}}{{ctx.Locale.Tr "repo.editor.commit_changes"}}{{end}}
 	</button>
-	<a class="ui button red" href="{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a>
+	<a class="ui button red" href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a>
 </div>
diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl
index cfc266731b..46f82c47d4 100644
--- a/templates/repo/editor/edit.tmpl
+++ b/templates/repo/editor/edit.tmpl
@@ -15,13 +15,13 @@
 					{{range $i, $v := .TreeNames}}
 						<div class="breadcrumb-divider">/</div>
 						{{if eq $i $l}}
-							<input id="file-name" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
+							<input id="file-name" maxlength="500" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
 							<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
 						{{else}}
 							<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
 						{{end}}
 					{{end}}
-					<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}{{if not .IsNewFile}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
+					<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}{{if not .IsNewFile}}/{{PathEscapeSegments .TreePath}}{{end}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
 					<input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required>
 				</div>
 			</div>
@@ -30,11 +30,11 @@
 					<a class="active item" data-tab="write">{{svg "octicon-code"}} {{if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.new_file"}}{{else}}{{ctx.Locale.Tr "repo.editor.edit_file"}}{{end}}</a>
 					<a class="item" data-tab="preview" data-url="{{.Repository.Link}}/markup" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL}}" data-markup-mode="file">{{svg "octicon-eye"}} {{ctx.Locale.Tr "preview"}}</a>
 					{{if not .IsNewFile}}
-					<a class="item" data-tab="diff" data-url="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}" data-context="{{.BranchLink}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a>
+					<a class="item" data-tab="diff" hx-params="context,content" hx-vals='{"context":"{{.BranchLink}}"}' hx-include="#edit_area" hx-swap="innerHTML" hx-target=".tab[data-tab='diff']" hx-indicator=".tab[data-tab='diff']" hx-post="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a>
 					{{end}}
 				</div>
 				<div class="ui bottom attached active tab segment" data-tab="write">
-					<textarea id="edit_area" name="content" class="gt-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}"
+					<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}"
 						data-url="{{.Repository.Link}}/markup"
 						data-context="{{.RepoLink}}"
 						data-previewable-extensions="{{.PreviewableExtensions}}"
@@ -45,7 +45,7 @@
 					{{ctx.Locale.Tr "loading"}}
 				</div>
 				<div class="ui bottom attached tab segment diff edit-diff" data-tab="diff">
-					{{ctx.Locale.Tr "loading"}}
+					<div class="tw-p-16"></div>
 				</div>
 			</div>
 			{{template "repo/editor/commit_form" .}}
diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl
index 44c30bd5f9..ff5c09667f 100644
--- a/templates/repo/editor/patch.tmpl
+++ b/templates/repo/editor/patch.tmpl
@@ -14,16 +14,16 @@
 					<div class="breadcrumb-divider">:</div>
 					<a class="section" href="{{$.BranchLink}}">{{.BranchName}}</a>
 					<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
-					<input type="hidden" id="tree_path" name="tree_path" value="" required>
+					<input type="hidden" name="tree_path" value="__dummy_for_EditRepoFileForm.TreePath(Required)__">
 					<input id="file-name" type="hidden" value="diff.patch">
 				</div>
 			</div>
 			<div class="field">
 				<div class="ui top attached tabular menu" data-write="write">
-					<a class="active item" data-tab="write">{{svg "octicon-code" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.editor.new_patch"}}</a>
+					<a class="active item" data-tab="write">{{svg "octicon-code" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.editor.new_patch"}}</a>
 				</div>
 				<div class="ui bottom attached active tab segment" data-tab="write">
-					<textarea id="edit_area" name="content" class="gt-hidden" data-id="repo-{{.Repository.Name}}-patch"
+					<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-patch"
 						data-context="{{.RepoLink}}"
 						data-line-wrap-extensions="{{.LineWrapExtensions}}">
 {{.FileContent}}</textarea>
diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl
index d362a5602a..0a7c49dae3 100644
--- a/templates/repo/editor/upload.tmpl
+++ b/templates/repo/editor/upload.tmpl
@@ -13,7 +13,7 @@
 					{{range $i, $v := .TreeNames}}
 						<div class="breadcrumb-divider">/</div>
 						{{if eq $i $l}}
-							<input type="text" id="file-name" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus>
+							<input type="text" id="file-name" maxlength="500" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus>
 							<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
 						{{else}}
 							<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl
index c1ec483b77..cb2a5ba1e9 100644
--- a/templates/repo/empty.tmpl
+++ b/templates/repo/empty.tmpl
@@ -6,11 +6,11 @@
 			<div class="sixteen wide column content">
 				{{template "base/alert" .}}
 				{{if .Repository.IsArchived}}
-					<div class="ui warning message gt-text-center">
+					<div class="ui warning message tw-text-center">
 						{{if .Repository.ArchivedUnix.IsZero}}
 							{{ctx.Locale.Tr "repo.archive.title"}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix) | Safe}}
+							{{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix)}}
 						{{end}}
 					</div>
 				{{end}}
@@ -24,7 +24,7 @@
 					</h4>
 					<div class="ui attached guide table segment empty-repo-guide">
 						<div class="item">
-							<h3>{{ctx.Locale.Tr "repo.clone_this_repo"}} <small>{{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository" | Str2html}}</small></h3>
+							<h3>{{ctx.Locale.Tr "repo.clone_this_repo"}} <small>{{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository"}}</small></h3>
 
 							<div class="repo-button-row">
 								{{if and .CanWriteCode (not .Repository.IsArchived)}}
@@ -37,14 +37,14 @@
 									</a>
 									{{end}}
 								{{end}}
-								<div class="ui action small input gt-df gt-f1">
+								<div class="clone-panel ui action small input tw-flex-1">
 									{{template "repo/clone_buttons" .}}
 								</div>
 							</div>
 						</div>
 
 						{{if not .Repository.IsArchived}}
-							<div class="divider gt-my-0"></div>
+							<div class="divider tw-my-0"></div>
 
 							<div class="item">
 								<h3>{{ctx.Locale.Tr "repo.create_new_repo_command"}}</h3>
diff --git a/templates/repo/file_info.tmpl b/templates/repo/file_info.tmpl
index 33f0f87d61..86c613e3a1 100644
--- a/templates/repo/file_info.tmpl
+++ b/templates/repo/file_info.tmpl
@@ -1,4 +1,4 @@
-<div class="file-info text grey normal gt-mono">
+<div class="file-info text grey normal tw-font-mono">
 	{{if .FileIsSymlink}}
 		<div class="file-info-entry">
 			{{ctx.Locale.Tr "repo.symbolic_link"}}
@@ -16,7 +16,7 @@
 	{{end}}
 	{{if .LFSLock}}
 		<div class="file-info-entry ui" data-tooltip-content="{{.LFSLockHint}}">
-			{{svg "octicon-lock" 16 "gt-mr-2"}}
+			{{svg "octicon-lock" 16 "tw-mr-1"}}
 			<a href="{{.LFSLockOwnerHomeLink}}">{{.LFSLockOwner}}</a>
 		</div>
 	{{end}}
diff --git a/templates/repo/find/files.tmpl b/templates/repo/find/files.tmpl
index eac6ec2011..548ce2f0e8 100644
--- a/templates/repo/find/files.tmpl
+++ b/templates/repo/find/files.tmpl
@@ -2,10 +2,10 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="gt-df gt-ac">
+		<div class="tw-flex tw-items-center">
 			<a href="{{$.RepoLink}}">{{.RepoName}}</a>
-			<span class="gt-mx-3">/</span>
-			<div class="ui input gt-f1">
+			<span class="tw-mx-2">/</span>
+			<div class="ui input tw-flex-1">
 				<input id="repo-file-find-input" type="text" autofocus data-url-data-link="{{.DataLink}}" data-url-tree-link="{{.TreeLink}}">
 			</div>
 		</div>
@@ -13,7 +13,7 @@
 			<tbody>
 			</tbody>
 		</table>
-		<div id="repo-find-file-no-result" class="ui row center gt-mt-5 gt-hidden">
+		<div id="repo-find-file-no-result" class="ui row center tw-mt-8 tw-hidden">
 			<h3>{{ctx.Locale.Tr "repo.find_file.no_matching"}}</h3>
 		</div>
 	</div>
diff --git a/templates/repo/forks.tmpl b/templates/repo/forks.tmpl
index b27b55c131..412c59b60e 100644
--- a/templates/repo/forks.tmpl
+++ b/templates/repo/forks.tmpl
@@ -6,8 +6,8 @@
 			{{ctx.Locale.Tr "repo.forks"}}
 		</h2>
 		{{range .Forks}}
-			<div class="gt-df gt-ac gt-py-3">
-				<span class="gt-mr-2">{{ctx.AvatarUtils.Avatar .Owner}}</span>
+			<div class="tw-flex tw-items-center tw-py-2">
+				<span class="tw-mr-1">{{ctx.AvatarUtils.Avatar .Owner}}</span>
 				<a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / <a href="{{.Link}}">{{.Name}}</a>
 			</div>
 		{{end}}
diff --git a/templates/repo/graph.tmpl b/templates/repo/graph.tmpl
index 37305d278a..9eb4bd4ecb 100644
--- a/templates/repo/graph.tmpl
+++ b/templates/repo/graph.tmpl
@@ -12,7 +12,7 @@
 						<div class="menu">
 							<div class="item" data-value="...flow-hide-pr-refs">
 								<span class="truncate">
-									{{svg "octicon-eye-closed" 16 "gt-mr-2"}}<span title="{{ctx.Locale.Tr "repo.commit_graph.hide_pr_refs"}}">{{ctx.Locale.Tr "repo.commit_graph.hide_pr_refs"}}</span>
+									{{svg "octicon-eye-closed" 16 "tw-mr-1"}}<span title="{{ctx.Locale.Tr "repo.commit_graph.hide_pr_refs"}}">{{ctx.Locale.Tr "repo.commit_graph.hide_pr_refs"}}</span>
 								</span>
 							</div>
 							{{range .AllRefs}}
@@ -20,37 +20,37 @@
 								{{if eq $refGroup "pull"}}
 									<div class="item" data-value="{{.Name}}">
 										<span class="truncate">
-											{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}<span title="{{.ShortName}}">#{{.ShortName}}</span>
+											{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}<span title="{{.ShortName}}">#{{.ShortName}}</span>
 										</span>
 									</div>
 								{{else if eq $refGroup "tags"}}
 									<div class="item" data-value="{{.Name}}">
 										<span class="truncate">
-											{{svg "octicon-tag" 16 "gt-mr-2"}}<span title="{{.ShortName}}">{{.ShortName}}</span>
+											{{svg "octicon-tag" 16 "tw-mr-1"}}<span title="{{.ShortName}}">{{.ShortName}}</span>
 										</span>
 									</div>
 								{{else if eq $refGroup "remotes"}}
 									<div class="item" data-value="{{.Name}}">
 										<span class="truncate">
-											{{svg "octicon-cross-reference" 16 "gt-mr-2"}}<span title="{{.ShortName}}">{{.ShortName}}</span>
+											{{svg "octicon-cross-reference" 16 "tw-mr-1"}}<span title="{{.ShortName}}">{{.ShortName}}</span>
 										</span>
 									</div>
 								{{else if eq $refGroup "heads"}}
 									<div class="item" data-value="{{.Name}}">
 										<span class="truncate">
-											{{svg "octicon-git-branch" 16 "gt-mr-2"}}<span title="{{.ShortName}}">{{.ShortName}}</span>
+											{{svg "octicon-git-branch" 16 "tw-mr-1"}}<span title="{{.ShortName}}">{{.ShortName}}</span>
 										</span>
 									</div>
 								{{end}}
 							{{end}}
 						</div>
 					</div>
-					<button id="flow-color-monochrome" class="ui labelled icon button{{if eq .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}">{{svg "material-invert-colors" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}</button>
-					<button id="flow-color-colored" class="ui labelled icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.color"}}">{{svg "material-palette" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.commit_graph.color"}}</button>
+					<button id="flow-color-monochrome" class="ui labelled icon button{{if eq .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}">{{svg "material-invert-colors" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}</button>
+					<button id="flow-color-colored" class="ui labelled icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.color"}}">{{svg "material-palette" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.commit_graph.color"}}</button>
 				</div>
 			</h2>
 			<div class="ui dividing"></div>
-			<div class="ui segment loading gt-hidden" id="loading-indicator"></div>
+			<div class="is-loading tw-py-32 tw-hidden" id="loading-indicator"></div>
 			{{template "repo/graph/svgcontainer" .}}
 			{{template "repo/graph/commits" .}}
 		</div>
diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl
index 61ef1fe10d..f141dbeada 100644
--- a/templates/repo/graph/commits.tmpl
+++ b/templates/repo/graph/commits.tmpl
@@ -28,10 +28,10 @@
 							{{- end -}}
 						</a>
 					</span>
-					<span class="message gt-dib gt-ellipsis gt-mr-3">
+					<span class="message tw-inline-block gt-ellipsis tw-mr-2">
 						<span>{{RenderCommitMessage $.Context $commit.Subject ($.Repository.ComposeMetas ctx)}}</span>
 					</span>
-					<span class="commit-refs gt-df gt-ac gt-mr-2">
+					<span class="commit-refs tw-flex tw-items-center tw-mr-1">
 						{{range $commit.Refs}}
 							{{$refGroup := .RefGroup}}
 							{{if eq $refGroup "pull"}}
@@ -58,20 +58,20 @@
 							{{end}}
 						{{end}}
 					</span>
-					<span class="author gt-df gt-ac gt-mr-3">
+					<span class="author tw-flex tw-items-center tw-mr-2">
 						{{$userName := $commit.Commit.Author.Name}}
 						{{if $commit.User}}
-							{{if $commit.User.FullName}}
+							{{if and $commit.User.FullName DefaultShowFullName}}
 								{{$userName = $commit.User.FullName}}
 							{{end}}
-							<span class="gt-mr-2">{{ctx.AvatarUtils.Avatar $commit.User}}</span>
+							<span class="tw-mr-1">{{ctx.AvatarUtils.Avatar $commit.User}}</span>
 							<a href="{{$commit.User.HomeLink}}">{{$userName}}</a>
 						{{else}}
-							<span class="gt-mr-2">{{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $userName}}</span>
+							<span class="tw-mr-1">{{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $userName}}</span>
 							{{$userName}}
 						{{end}}
 					</span>
-					<span class="time gt-df gt-ac">{{DateTime "full" $commit.Date}}</span>
+					<span class="time tw-flex tw-items-center">{{DateTime "full" $commit.Date}}</span>
 				{{end}}
 			</li>
 		{{end}}
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 93102467cc..5e2774dfa1 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -1,31 +1,33 @@
-<div class="header-wrapper">
+<div class="secondary-nav">
 {{with .Repository}}
 	<div class="ui container">
 		<div class="repo-header">
-			<div class="flex-item gt-ac">
-				<div class="flex-item-leading">{{template "repo/icon" .}}</div>
+			<div class="flex-item tw-items-center">
+				<div class="flex-item-leading">
+					{{template "repo/icon" .}}
+				</div>
 				<div class="flex-item-main">
-					<div class="flex-item-title">
-						<a class="text light thin" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/
-						<a class="text primary" href="{{$.RepoLink}}">{{.Name}}</a></div>
+					<div class="flex-item-title tw-text-18">
+						<a class="muted tw-font-normal" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/<a class="muted" href="{{$.RepoLink}}">{{.Name}}</a>
+					</div>
 				</div>
 				<div class="flex-item-trailing">
 					{{if .IsArchived}}
-						<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.archived"}}</span>
-						<div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.archived"}}">{{svg "octicon-archive" 18}}</div>
+						<span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.archived"}}</span>
+						<div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.archived"}}">{{svg "octicon-archive" 18}}</div>
 					{{end}}
 					{{if .IsPrivate}}
-						<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.private"}}</span>
-						<div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.private"}}">{{svg "octicon-lock" 18}}</div>
+						<span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.private"}}</span>
+						<div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.private"}}">{{svg "octicon-lock" 18}}</div>
 					{{else}}
 						{{if .Owner.Visibility.IsPrivate}}
-							<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.internal"}}</span>
-							<div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.internal"}}">{{svg "octicon-shield-lock" 18}}</div>
+							<span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.internal"}}</span>
+							<div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.internal"}}">{{svg "octicon-shield-lock" 18}}</div>
 						{{end}}
 					{{end}}
 					{{if .IsTemplate}}
-						<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.template"}}</span>
-						<div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.template"}}">{{svg "octicon-repo-template" 18}}</div>
+						<span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.template"}}</span>
+						<div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.template"}}">{{svg "octicon-repo-template" 18}}</div>
 					{{end}}
 					{{if eq .ObjectFormatName "sha256"}}
 						<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.sha256"}}</span>
@@ -76,34 +78,34 @@
 							<a class="ui compact{{if $.ShowForkModal}} show-modal{{end}} small basic button"
 								{{if not $.CanSignedUserFork}}
 									{{if gt (len $.UserAndOrgForks) 1}}
-										data-modal="#fork-repo-modal"
+										href="#" data-modal="#fork-repo-modal"
 									{{else if eq (len $.UserAndOrgForks) 1}}
 										href="{{AppSubUrl}}/{{(index $.UserAndOrgForks 0).FullName}}"
 									{{/*else is not required here, because the button shouldn't link to any site if you can't create a fork*/}}
 									{{end}}
 								{{else if not $.UserAndOrgForks}}
-									href="{{AppSubUrl}}/repo/fork/{{.ID}}"
+									href="{{$.RepoLink}}/fork"
 								{{else}}
-									data-modal="#fork-repo-modal"
+									href="#" data-modal="#fork-repo-modal"
 								{{end}}
 							>
-								{{svg "octicon-repo-forked"}}<span class="text">{{ctx.Locale.Tr "repo.fork"}}</span>
+								{{svg "octicon-repo-forked"}}<span class="text not-mobile">{{ctx.Locale.Tr "repo.fork"}}</span>
 							</a>
 							<div class="ui small modal" id="fork-repo-modal">
 								<div class="header">
 									{{ctx.Locale.Tr "repo.already_forked" .Name}}
 								</div>
-								<div class="content gt-text-left">
+								<div class="content tw-text-left">
 									<div class="ui list">
 										{{range $.UserAndOrgForks}}
-											<div class="ui item gt-py-3">
-												<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "gt-mr-3"}}{{.FullName}}</a>
+											<div class="ui item tw-py-2">
+												<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "tw-mr-2"}}{{.FullName}}</a>
 											</div>
 										{{end}}
 									</div>
 									{{if $.CanSignedUserFork}}
 									<div class="divider"></div>
-									<a href="{{AppSubUrl}}/repo/fork/{{.ID}}">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
+									<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
 									{{end}}
 								</div>
 							</div>
@@ -126,9 +128,9 @@
 		{{if .IsGenerated}}<div class="fork-flag">{{ctx.Locale.Tr "repo.generated_from"}} <a href="{{(.TemplateRepo ctx).Link}}">{{(.TemplateRepo ctx).FullName}}</a></div>{{end}}
 	</div>
 {{end}}
-	<div class="ui container secondary pointing tabular top attached borderless menu new-menu navbar">
+	<overflow-menu class="ui container secondary pointing tabular top attached borderless menu tw-pt-0 tw-my-0">
 		{{if not (or .Repository.IsBeingCreated .Repository.IsBroken)}}
-			<div class="new-menu-inner">
+			<div class="overflow-menu-items">
 				{{if .Permission.CanRead $.UnitTypeCode}}
 				<a class="{{if .PageIsViewCode}}active {{end}}item" href="{{.RepoLink}}{{if and (ne .BranchName .Repository.DefaultBranch) (not $.PageIsWiki)}}/src/{{.BranchNameSubURL}}{{end}}">
 					{{svg "octicon-code"}} {{ctx.Locale.Tr "repo.code"}}
@@ -174,7 +176,8 @@
 					</a>
 				{{end}}
 
-				{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects)}}
+				{{$projectsUnit := .Repository.MustGetUnit $.Context $.UnitTypeProjects}}
+				{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}}
 					<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
 						{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}}
 						{{if .Repository.NumOpenProjects}}
@@ -219,12 +222,12 @@
 				{{end}}
 			</div>
 		{{else if .Permission.IsAdmin}}
-			<div class="new-menu-inner">
+			<div class="overflow-menu-items">
 				<a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings">
 					{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
 				</a>
 			</div>
 		{{end}}
-	</div>
+	</overflow-menu>
 	<div class="ui tabs divider"></div>
 </div>
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index d91dc4394e..e18a0aec17 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -5,46 +5,34 @@
 		{{template "base/alert" .}}
 		{{template "repo/code/recently_pushed_new_branches" .}}
 		{{if and (not .HideRepoInfo) (not .IsBlame)}}
-		<div class="ui repo-description gt-word-break">
-			<div id="repo-desc" class="gt-font-16">
+		<div class="repo-description">
+			<div id="repo-desc" class="gt-word-break tw-text-16">
 				{{$description := .Repository.DescriptionHTML $.Context}}
 				{{if $description}}<span class="description">{{$description | RenderCodeBlock}}</span>{{else if .IsRepositoryAdmin}}<span class="no-description text-italic">{{ctx.Locale.Tr "repo.no_desc"}}</span>{{end}}
 				<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>
 			</div>
-			{{if .RepoSearchEnabled}}
-				<div class="ui repo-search">
-					<form class="ui form ignore-dirty" action="{{.RepoLink}}/search" method="get">
-						<div class="field">
-							<div class="ui small action input{{if .CodeIndexerUnavailable}} disabled left icon{{end}}"{{if .CodeIndexerUnavailable}} data-tooltip-content="{{ctx.Locale.Tr "repo.search.code_search_unavailable"}}"{{end}}>
-								<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "repo.search.search_repo"}}">
-								{{if .CodeIndexerUnavailable}}
-									<i class="icon">{{svg "octicon-alert"}}</i>
-								{{end}}
-								<button class="ui small icon button"{{if .CodeIndexerUnavailable}} disabled{{end}} type="submit">
-									{{svg "octicon-search"}}
-								</button>
-							</div>
-						</div>
-					</form>
+			<form class="ignore-dirty" action="{{.RepoLink}}/search" method="get">
+				<div class="ui small action input">
+					<input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
+					{{template "shared/search/button"}}
 				</div>
-			{{end}}
+			</form>
 		</div>
-		<div class="gt-df gt-ac gt-fw gt-gap-2" id="repo-topics">
-			{{range .Topics}}<a class="ui repo-topic large label topic gt-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
-			{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg gt-font-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
+		<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-my-2" id="repo-topics">
+			{{/* it should match the code in issue-home.js */}}
+			{{range .Topics}}<a class="repo-topic ui large label" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
+			{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
 		</div>
 		{{end}}
 		{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
-		<div class="ui form gt-hidden gt-df gt-fc gt-mt-4" id="topic_edit">
-			<div class="field gt-f1 gt-mb-2">
-				<div class="ui fluid multiple search selection dropdown gt-fw" data-text-count-prompt="{{ctx.Locale.Tr "repo.topic.count_prompt"}}" data-text-format-prompt="{{ctx.Locale.Tr "repo.topic.format_prompt"}}">
-					<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
-					{{range .Topics}}
-						{{/* keey the same layout as Fomantic UI generated labels */}}
-						<a class="ui label transition visible gt-cursor-default gt-dib" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
-					{{end}}
-					<div class="text"></div>
-				</div>
+		<div class="ui form tw-hidden tw-flex tw-gap-2 tw-my-2" id="topic_edit">
+			<div class="ui fluid multiple search selection dropdown tw-flex-wrap tw-flex-1">
+				<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
+				{{range .Topics}}
+					{{/* keep the same layout as Fomantic UI generated labels */}}
+					<a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
+				{{end}}
+				<div class="text"></div>
 			</div>
 			<div>
 				<button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button>
@@ -53,18 +41,21 @@
 		</div>
 		{{end}}
 		{{if .Repository.IsArchived}}
-			<div class="ui warning message gt-text-center">
+			<div class="ui warning message tw-text-center">
 				{{if .Repository.ArchivedUnix.IsZero}}
 					{{ctx.Locale.Tr "repo.archive.title"}}
 				{{else}}
-					{{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix) | Safe}}
+					{{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix)}}
 				{{end}}
 			</div>
 		{{end}}
 		{{template "repo/sub_menu" .}}
+		{{$n := len .TreeNames}}
+		{{$l := Eval $n "-" 1}}
+		{{$isHomepage := (eq $n 0)}}
 		<div class="repo-button-row">
-			<div class="gt-df gt-ac gt-fw gt-gap-y-3">
-				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "gt-mr-2"}}
+			<div class="tw-flex tw-items-center tw-gap-y-2">
+				{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}}
 				{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
 					{{$cmpBranch := ""}}
 					{{if ne .Repository.ID .BaseRepo.ID}}
@@ -78,14 +69,12 @@
 					</a>
 				{{end}}
 				<!-- Show go to file and breadcrumbs if not on home page -->
-				{{$n := len .TreeNames}}
-				{{$l := Eval $n "-" 1}}
-				{{if eq $n 0}}
+				{{if $isHomepage}}
 					<a href="{{.Repository.Link}}/find/{{.BranchNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a>
 				{{end}}
 
 				{{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
-					<button class="ui dropdown basic compact jump button gt-mr-2"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
+					<button class="ui dropdown basic compact jump button tw-mr-1"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
 						{{ctx.Locale.Tr "repo.editor.add_file"}}
 						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 						<div class="menu">
@@ -104,51 +93,53 @@
 					</button>
 				{{end}}
 
-				{{if and (eq $n 0) (.Repository.IsTemplate)}}
+				{{if and $isHomepage (.Repository.IsTemplate)}}
 					<a role="button" class="ui primary compact button" href="{{AppSubUrl}}/repo/create?template_id={{.Repository.ID}}">
 						{{ctx.Locale.Tr "repo.use_template"}}
 					</a>
 				{{end}}
-				{{if ne $n 0}}
-					<span class="breadcrumb repo-path gt-ml-2">
+				{{if (not $isHomepage)}}
+					<span class="breadcrumb repo-path tw-ml-1">
 						<a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
 						{{- range $i, $v := .TreeNames -}}
 							<span class="breadcrumb-divider">/</span>
 							{{- if eq $i $l -}}
-								<span class="active section" title="{{$v}}">{{StringUtils.EllipsisString $v 30}}</span>
+								<span class="active section" title="{{$v}}">{{$v}}</span>
 							{{- else -}}
-								{{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{StringUtils.EllipsisString $v 30}}</a></span>
+								{{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{$v}}</a></span>
 							{{- end -}}
 						{{- end -}}
 					</span>
 				{{end}}
 			</div>
-			<div class="gt-df gt-ac">
+			<div class="tw-flex tw-items-center">
 				<!-- Only show clone panel in repository home page -->
-				{{if eq $n 0}}
-					<div class="ui action tiny input" id="clone-panel">
+				{{if $isHomepage}}
+					<div class="clone-panel ui action tiny input">
 						{{template "repo/clone_buttons" .}}
-						<button id="more-btn" class="ui basic small compact jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
+						<button class="ui small jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
 							{{svg "octicon-kebab-horizontal"}}
 							<div class="menu">
 								{{if not $.DisableDownloadSourceArchives}}
-									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
-									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
-									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a>
-									{{if .CitiationExist}}
-										<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
-									{{end}}
+									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
+									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
+									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a>
+								{{end}}
+								{{if .CitiationExist}}
+									<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
+								{{end}}
+								{{range .OpenWithEditorApps}}
+									<a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a>
 								{{end}}
-								<a class="item js-clone-url-vsc" href="vscode://vscode.git/clone?url={{.CloneButtonOriginLink.HTTPS}}">{{svg "gitea-vscode" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.clone_in_vsc"}}</a>
 							</div>
 						</button>
 						{{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}}
 					</div>
 					{{template "repo/cite/cite_modal" .}}
 				{{end}}
-				{{if and (ne $n 0) (not .IsViewFile) (not .IsBlame)}}
+				{{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}}
 					<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
-						{{svg "octicon-history" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.file_history"}}
+						{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
 					</a>
 				{{end}}
 			</div>
diff --git a/templates/repo/icon.tmpl b/templates/repo/icon.tmpl
index 5a80b959d0..e5e0bd68e7 100644
--- a/templates/repo/icon.tmpl
+++ b/templates/repo/icon.tmpl
@@ -1,10 +1,10 @@
 {{$avatarLink := (.RelAvatarLink ctx)}}
 {{if $avatarLink}}
-	<img class="ui avatar gt-vm" src="{{$avatarLink}}" width="32" height="32" alt="{{.FullName}}">
+	<img class="ui avatar tw-align-middle" src="{{$avatarLink}}" width="24" height="24" alt="{{.FullName}}">
 {{else if $.IsMirror}}
-	{{svg "octicon-mirror" 32}}
+	{{svg "octicon-mirror" 24}}
 {{else if $.IsFork}}
-	{{svg "octicon-repo-forked" 32}}
+	{{svg "octicon-repo-forked" 24}}
 {{else}}
-	{{svg "octicon-repo" 32}}
+	{{svg "octicon-repo" 24}}
 {{end}}
diff --git a/templates/repo/issue/branch_selector_field.tmpl b/templates/repo/issue/branch_selector_field.tmpl
index 9b7a05ce35..b8ac9a6194 100644
--- a/templates/repo/issue/branch_selector_field.tmpl
+++ b/templates/repo/issue/branch_selector_field.tmpl
@@ -4,8 +4,8 @@
 <form method="post" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref" id="update_issueref_form">
 	{{$.CsrfTokenHtml}}
 </form>
-
-<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating filter select-branch dropdown" data-no-results="{{ctx.Locale.Tr "repo.pulls.no_results"}}">
+{{/* TODO: share this branch selector dropdown with the same in repo page */}}
+<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating filter select-branch dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}">
 	<div class="ui basic small button">
 		<span class="text branch-name">{{if .Reference}}{{$.RefEndName}}{{else}}{{ctx.Locale.Tr "repo.issues.no_ref"}}{{end}}</span>
 		{{if .HasIssuesOrPullsWritePermission}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}{{end}}
@@ -18,33 +18,38 @@
 		<div class="header">
 			<div class="ui grid">
 				<div class="two column row">
-					<a class="reference column" href="#" data-target="#branch-list">
+					<a class="reference column muted" href="#" data-target="#branch-list">
 						<span class="text black">
-							{{svg "octicon-git-branch" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.branches"}}
+							{{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}}
 						</span>
 					</a>
-					<a class="reference column" href="#" data-target="#tag-list">
+					<a class="reference column muted" href="#" data-target="#tag-list">
 						<span class="text">
-							{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.tags"}}
+							{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}}
 						</span>
 					</a>
 				</div>
 			</div>
 		</div>
+		<div class="branch-tag-divider"></div>
 		<div id="branch-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}}">
 			{{if .Reference}}
 				<div class="item text small" data-id="" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div>
 			{{end}}
 			{{range .Branches}}
 				<div class="item" data-id="refs/heads/{{.}}" data-name="{{.}}" data-id-selector="#ref_selector">{{.}}</div>
+			{{else}}
+				<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
 			{{end}}
 		</div>
-		<div id="tag-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}} gt-hidden">
+		<div id="tag-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}} tw-hidden">
 			{{if .Reference}}
 				<div class="item text small" data-id="" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div>
 			{{end}}
 			{{range .Tags}}
 				<div class="item" data-id="refs/tags/{{.}}" data-name="tags/{{.}}" data-id-selector="#ref_selector">{{.}}</div>
+			{{else}}
+				<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
 			{{end}}
 		</div>
 	</div>
diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl
index 14d08fc0ef..bb9340bb2e 100644
--- a/templates/repo/issue/card.tmpl
+++ b/templates/repo/issue/card.tmpl
@@ -6,66 +6,66 @@
 			{{end}}
 		</div>
 	{{end}}
-	<div class="content gt-p-0 gt-w-full">
-		<div class="gt-df gt-items-start">
+	<div class="content tw-p-0 tw-w-full">
+		<div class="tw-flex tw-items-start">
 			<div class="issue-card-icon">
 				{{template "shared/issueicon" .}}
 			</div>
 			<a class="issue-card-title muted issue-title" href="{{.Link}}">{{.Title | RenderEmoji ctx | RenderCodeBlock}}</a>
 			{{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}}
-				<a role="button" class="issue-card-unpin muted gt-df gt-ac" data-tooltip-content={{ctx.Locale.Tr "repo.issues.unpin_issue"}} data-issue-id="{{.ID}}" data-unpin-url="{{$.Page.Link}}/unpin/{{.Index}}">
+				<a role="button" class="issue-card-unpin muted tw-flex tw-items-center" data-tooltip-content={{ctx.Locale.Tr "repo.issues.unpin_issue"}} data-issue-id="{{.ID}}" data-unpin-url="{{$.Page.Link}}/unpin/{{.Index}}">
 					{{svg "octicon-x" 16}}
 				</a>
 			{{end}}
 		</div>
-		<div class="meta gt-my-2">
+		<div class="meta tw-my-1">
 			<span class="text light grey muted-links">
 				{{if not $.Page.Repository}}{{.Repo.FullName}}{{end}}#{{.Index}}
 				{{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}}
 				{{if .OriginalAuthor}}
-					{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}}
+					{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .OriginalAuthor}}
 				{{else if gt .Poster.ID 0}}
-					{{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape) | Safe}}
+					{{ctx.Locale.Tr .GetLastEventLabel $timeStr .Poster.HomeLink .Poster.GetDisplayName}}
 				{{else}}
-					{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape) | Safe}}
+					{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .Poster.GetDisplayName}}
 				{{end}}
 			</span>
 		</div>
 		{{if .MilestoneID}}
-		<div class="meta gt-my-2">
+		<div class="meta tw-my-1">
 			<a class="milestone" href="{{.Repo.Link}}/milestone/{{.MilestoneID}}">
-				{{svg "octicon-milestone" 16 "gt-mr-2 gt-vm"}}
-				<span class="gt-vm">{{.Milestone.Name}}</span>
+				{{svg "octicon-milestone" 16 "tw-mr-1 tw-align-middle"}}
+				<span class="tw-align-middle">{{.Milestone.Name}}</span>
 			</a>
 		</div>
 		{{end}}
 		{{if $.Page.LinkedPRs}}
 		{{range index $.Page.LinkedPRs .ID}}
-		<div class="meta gt-my-2">
+		<div class="meta tw-my-1">
 			<a href="{{$.Issue.Repo.Link}}/pulls/{{.Index}}">
-				<span class="gt-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "gt-mr-2 gt-vm"}}</span>
-				<span class="gt-vm">{{.Title}} <span class="text light grey">#{{.Index}}</span></span>
+				<span class="tw-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "tw-mr-1 tw-align-middle"}}</span>
+				<span class="tw-align-middle">{{.Title}} <span class="text light grey">#{{.Index}}</span></span>
 			</a>
 		</div>
 		{{end}}
 		{{end}}
 		{{$tasks := .GetTasks}}
 		{{if gt $tasks 0}}
-			<div class="meta gt-my-2">
-				{{svg "octicon-checklist" 16 "gt-mr-2 gt-vm"}}
-				<span class="gt-vm">{{.GetTasksDone}} / {{$tasks}}</span>
+			<div class="meta tw-my-1">
+				{{svg "octicon-checklist" 16 "tw-mr-1 tw-align-middle"}}
+				<span class="tw-align-middle">{{.GetTasksDone}} / {{$tasks}}</span>
 			</div>
 		{{end}}
 	</div>
 
 	{{if or .Labels .Assignees}}
-	<div class="extra content labels-list gt-p-0 gt-pt-2">
+	<div class="extra content labels-list tw-p-0 tw-pt-1">
 		{{range .Labels}}
-			<a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx .}}</a>
+			<a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx ctx.Locale .}}</a>
 		{{end}}
 		<div class="right floated">
 			{{range .Assignees}}
-				<a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 28 "mini gt-mr-3"}}</a>
+				<a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 28 "mini tw-mr-2"}}</a>
 			{{end}}
 		</div>
 	</div>
diff --git a/templates/repo/issue/choose.tmpl b/templates/repo/issue/choose.tmpl
index 127b9d7d87..38cf9e485f 100644
--- a/templates/repo/issue/choose.tmpl
+++ b/templates/repo/issue/choose.tmpl
@@ -3,7 +3,7 @@
 	{{template "repo/header" .}}
 	<div class="ui container">
 		{{template "base/alert" .}}
-		<div class="navbar">
+		<div class="issue-navbar">
 			{{template "repo/issue/navbar" .}}
 		</div>
 		<div class="divider"></div>
@@ -11,8 +11,8 @@
 			<div class="ui attached segment">
 				<div class="ui two column grid">
 					<div class="column left aligned">
-						<strong>{{.Name | RenderEmojiPlain}}</strong>
-						<br>{{.About | RenderEmojiPlain}}
+						<strong>{{.Name}}</strong>
+						<br>{{.About}}
 					</div>
 					<div class="column right aligned">
 						<a href="{{$.RepoLink}}/issues/new?template={{.FileName}}{{if $.milestone}}&milestone={{$.milestone}}{{end}}{{if $.project}}&project={{$.project}}{{end}}" class="ui primary button">{{ctx.Locale.Tr "repo.issues.choose.get_started"}}</a>
@@ -24,8 +24,8 @@
 			<div class="ui attached segment">
 				<div class="ui two column grid">
 					<div class="column left aligned">
-						<strong>{{.Name | RenderEmojiPlain}}</strong>
-						<br>{{.About | RenderEmojiPlain}}
+						<strong>{{.Name}}</strong>
+						<br>{{.About}}
 					</div>
 					<div class="column right aligned">
 						<a href="{{.URL}}" class="ui primary button">{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.issues.choose.open_external_link"}}</a>
diff --git a/templates/repo/issue/fields/checkboxes.tmpl b/templates/repo/issue/fields/checkboxes.tmpl
index 237f2eb5dd..531f401fb7 100644
--- a/templates/repo/issue/fields/checkboxes.tmpl
+++ b/templates/repo/issue/fields/checkboxes.tmpl
@@ -1,8 +1,8 @@
-<div class="field">
+<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}">
 	{{template "repo/issue/fields/header" .}}
 	{{range $i, $opt := .item.Attributes.options}}
 		<div class="field inline">
-			<div class="ui checkbox gt-mr-0">
+			<div class="ui checkbox tw-mr-0 {{if and ($opt.visible) (not (SliceUtils.Contains $opt.visible "form"))}}tw-hidden{{end}}">
 				<input type="checkbox" name="form-field-{{$.item.ID}}-{{$i}}" {{if $opt.required}}required{{end}}>
 				<label>{{RenderMarkdownToHtml $.context $opt.label}}</label>
 			</div>
diff --git a/templates/repo/issue/fields/dropdown.tmpl b/templates/repo/issue/fields/dropdown.tmpl
index 23aa373cd2..f4fa79738c 100644
--- a/templates/repo/issue/fields/dropdown.tmpl
+++ b/templates/repo/issue/fields/dropdown.tmpl
@@ -1,4 +1,4 @@
-<div class="field">
+<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}">
 	{{template "repo/issue/fields/header" .}}
 	{{/* FIXME: required validation */}}
 	<div class="ui fluid selection dropdown {{if .item.Attributes.multiple}}multiple clearable{{end}}">
diff --git a/templates/repo/issue/fields/input.tmpl b/templates/repo/issue/fields/input.tmpl
index 3fc8a86510..039f9a9f34 100644
--- a/templates/repo/issue/fields/input.tmpl
+++ b/templates/repo/issue/fields/input.tmpl
@@ -1,4 +1,4 @@
-<div class="field">
+<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}">
 	{{template "repo/issue/fields/header" .}}
 	<input type="{{if .item.Validations.is_number}}number{{else}}text{{end}}" name="form-field-{{.item.ID}}" placeholder="{{.item.Attributes.placeholder}}" value="{{.item.Attributes.value}}" {{if .item.Validations.required}}required{{end}} {{if .item.Validations.regex}}pattern="{{.item.Validations.regex}}" title="{{.item.Validations.regex}}"{{end}}>
 </div>
diff --git a/templates/repo/issue/fields/markdown.tmpl b/templates/repo/issue/fields/markdown.tmpl
index fd5b6afd22..934699ed05 100644
--- a/templates/repo/issue/fields/markdown.tmpl
+++ b/templates/repo/issue/fields/markdown.tmpl
@@ -1,3 +1,3 @@
-<div class="field">
+<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}">
 	<div>{{RenderMarkdownToHtml .Context .item.Attributes.value}}</div>
 </div>
diff --git a/templates/repo/issue/fields/textarea.tmpl b/templates/repo/issue/fields/textarea.tmpl
index 55adeb28d0..3ad69e1220 100644
--- a/templates/repo/issue/fields/textarea.tmpl
+++ b/templates/repo/issue/fields/textarea.tmpl
@@ -1,5 +1,5 @@
 {{$useMarkdownEditor := not .item.Attributes.render}}
-<div class="field {{if $useMarkdownEditor}}combo-editor-dropzone{{end}}">
+<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}} {{if $useMarkdownEditor}}combo-editor-dropzone{{end}}">
 	{{template "repo/issue/fields/header" .}}
 
 	{{/* the real form element to provide the value */}}
@@ -7,7 +7,7 @@
 
 	{{if $useMarkdownEditor}}
 		{{template "shared/combomarkdowneditor" (dict
-			"ContainerClasses" "gt-hidden"
+			"ContainerClasses" "tw-hidden"
 			"MarkdownPreviewUrl" (print .root.RepoLink "/markup")
 			"MarkdownPreviewContext" .root.RepoLink
 			"TextareaContent" .item.Attributes.value
@@ -16,7 +16,7 @@
 		)}}
 
 		{{if .root.IsAttachmentEnabled}}
-		<div class="gt-mt-4 form-field-dropzone gt-hidden">
+		<div class="tw-mt-4 form-field-dropzone tw-hidden">
 			{{template "repo/upload" .root}}
 		</div>
 		{{end}}
diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl
index a2296f6597..f23ca36d78 100644
--- a/templates/repo/issue/filter_actions.tmpl
+++ b/templates/repo/issue/filter_actions.tmpl
@@ -29,8 +29,8 @@
 						<div class="divider"></div>
 					{{end}}
 					{{$previousExclusiveScope = $exclusiveScope}}
-					<div class="item issue-action gt-df gt-sb" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/labels">
-						{{if SliceUtils.Contains $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context .}}
+					<div class="item issue-action tw-flex tw-justify-between" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/labels">
+						{{if SliceUtils.Contains $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context ctx.Locale .}}
 						{{template "repo/issue/labels/label_archived" .}}
 					</div>
 				{{end}}
@@ -85,7 +85,7 @@
 					</div>
 					{{range .OpenProjects}}
 						<div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/projects">
-							{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+							{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 						</div>
 					{{end}}
 				{{end}}
@@ -96,7 +96,7 @@
 					</div>
 					{{range .ClosedProjects}}
 						<div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/projects">
-							{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+							{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 						</div>
 					{{end}}
 				{{end}}
diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl
index 511ef7f397..c6de4977dc 100644
--- a/templates/repo/issue/filter_list.tmpl
+++ b/templates/repo/issue/filter_list.tmpl
@@ -16,15 +16,15 @@
 			>
 			<label for="archived-filter-checkbox">
 				{{ctx.Locale.Tr "repo.issues.label_archived_filter"}}
-				<i class="gt-ml-2" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>
+				<i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>
 					{{svg "octicon-info"}}
 				</i>
 			</label>
 		</div>
-		<span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude" | Safe}}</span>
+		<span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
 		<div class="divider"></div>
-		<a class="{{if .AllLabels}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a>
-		<a class="{{if .NoLabel}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels=0&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
+		<a class="{{if .AllLabels}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a>
+		<a class="{{if .NoLabel}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels=0&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
 		{{$previousExclusiveScope := "_no_scope"}}
 		{{range .Labels}}
 			{{$exclusiveScope := .ExclusiveScope}}
@@ -32,7 +32,7 @@
 				<div class="divider"></div>
 			{{end}}
 			{{$previousExclusiveScope = $exclusiveScope}}
-			<a class="item label-filter-item gt-df gt-ac" {{if .IsArchived}}data-is-archived{{end}} href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}">
+			<a class="item label-filter-item tw-flex tw-items-center" {{if .IsArchived}}data-is-archived{{end}} href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}">
 				{{if .IsExcluded}}
 					{{svg "octicon-circle-slash"}}
 				{{else if .IsSelected}}
@@ -42,8 +42,8 @@
 						{{svg "octicon-check"}}
 					{{end}}
 				{{end}}
-				{{RenderLabel $.Context .}}
-				<p class="gt-ml-auto">{{template "repo/issue/labels/label_archived" .}}</p>
+				{{RenderLabel $.Context ctx.Locale .}}
+				<p class="tw-ml-auto">{{template "repo/issue/labels/label_archived" .}}</p>
 			</a>
 		{{end}}
 	</div>
@@ -62,13 +62,13 @@
 			<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestone"}}">
 		</div>
 		<div class="divider"></div>
-		<a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=0&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a>
-		<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=-1&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a>
+		<a class="{{if not $.MilestoneID}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=0&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a>
+		<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=-1&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a>
 		{{if .OpenMilestones}}
 			<div class="divider"></div>
 			<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div>
 			{{range .OpenMilestones}}
-			<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
+			<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
 				{{svg "octicon-milestone" 16 "mr-2"}}
 				{{.Name}}
 			</a>
@@ -78,7 +78,7 @@
 			<div class="divider"></div>
 			<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}</div>
 			{{range .ClosedMilestones}}
-			<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
+			<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
 				{{svg "octicon-milestone" 16 "mr-2"}}
 				{{.Name}}
 			</a>
@@ -99,16 +99,16 @@
 			<i class="icon">{{svg "octicon-search" 16}}</i>
 			<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_project"}}">
 		</div>
-		<a class="{{if not .ProjectID}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_all"}}</a>
-		<a class="{{if eq .ProjectID -1}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&project=-1&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_none"}}</a>
+		<a class="{{if not .ProjectID}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_all"}}</a>
+		<a class="{{if eq .ProjectID -1}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&project=-1&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_none"}}</a>
 		{{if .OpenProjects}}
 			<div class="divider"></div>
 			<div class="header">
 				{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
 			</div>
 			{{range .OpenProjects}}
-				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item gt-df" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
-					{{svg .IconName 18 "gt-mr-3 gt-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span>
+				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item tw-flex" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
+					{{svg .IconName 18 "tw-mr-2 tw-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span>
 				</a>
 			{{end}}
 		{{end}}
@@ -118,8 +118,8 @@
 				{{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
 			</div>
 			{{range .ClosedProjects}}
-				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
-					{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+				<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
+					{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 				</a>
 			{{end}}
 		{{end}}
@@ -130,7 +130,7 @@
 <div class="ui dropdown jump item user-remote-search" data-tooltip-content="{{ctx.Locale.Tr "repo.author_search_tooltip"}}"
 	data-search-url="{{if .Milestone}}{{$.RepoLink}}/issues/posters{{else}}{{$.Link}}/posters{{end}}"
 	data-selected-user-id="{{$.PosterID}}"
-	data-action-jump-url="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={user_id}{{if $.ShowArchivedLabels}}&archived=true{{end}}"
+	data-action-jump-url="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={user_id}{{if $.ShowArchivedLabels}}&archived=true{{end}}"
 >
 	<span class="text">
 		{{ctx.Locale.Tr "repo.issues.filter_poster"}}
@@ -156,11 +156,11 @@
 			<i class="icon">{{svg "octicon-search" 16}}</i>
 			<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignee"}}">
 		</div>
-		<a class="{{if not .AssigneeID}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}}</a>
-		<a class="{{if eq .AssigneeID -1}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee=-1&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}</a>
+		<a class="{{if not .AssigneeID}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}}</a>
+		<a class="{{if eq .AssigneeID -1}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee=-1&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}</a>
 		<div class="divider"></div>
 		{{range .Assignees}}
-			<a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item gt-df" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{.ID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
+			<a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item tw-flex" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{.ID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
 				{{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}}
 			</a>
 		{{end}}
@@ -175,14 +175,14 @@
 		</span>
 		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 		<div class="menu">
-			<a class="{{if eq .ViewType "all"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.all_issues"}}</a>
-			<a class="{{if eq .ViewType "assigned"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}</a>
-			<a class="{{if eq .ViewType "created_by"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}</a>
+			<a class="{{if eq .ViewType "all"}}active {{end}}item" href="?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.all_issues"}}</a>
+			<a class="{{if eq .ViewType "assigned"}}active {{end}}item" href="?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}</a>
+			<a class="{{if eq .ViewType "created_by"}}active {{end}}item" href="?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}</a>
 			{{if .PageIsPullList}}
-				<a class="{{if eq .ViewType "review_requested"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=review_requested&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}</a>
-				<a class="{{if eq .ViewType "reviewed_by"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=reviewed_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}</a>
+				<a class="{{if eq .ViewType "review_requested"}}active {{end}}item" href="?q={{$.Keyword}}&type=review_requested&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}</a>
+				<a class="{{if eq .ViewType "reviewed_by"}}active {{end}}item" href="?q={{$.Keyword}}&type=reviewed_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}</a>
 			{{end}}
-			<a class="{{if eq .ViewType "mentioned"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}</a>
+			<a class="{{if eq .ViewType "mentioned"}}active {{end}}item" href="?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}</a>
 		</div>
 	</div>
 {{end}}
@@ -194,13 +194,13 @@
 	</span>
 	{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 	<div class="menu">
-		<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-		<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-		<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-		<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-		<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
-		<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
-		<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
-		<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=farduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
+		<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
+		<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+		<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+		<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+		<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
+		<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
+		<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
+		<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=farduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
 	</div>
 </div>
diff --git a/templates/repo/issue/filters.tmpl b/templates/repo/issue/filters.tmpl
index 56c65e2401..06e7c1aa6c 100644
--- a/templates/repo/issue/filters.tmpl
+++ b/templates/repo/issue/filters.tmpl
@@ -1,7 +1,7 @@
 <div id="issue-filters" class="issue-list-toolbar">
 	<div class="issue-list-toolbar-left">
 		{{if and $.CanWriteIssuesOrPulls .Issues}}
-			<input type="checkbox" autocomplete="off" class="issue-checkbox-all gt-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}">
+			<input type="checkbox" autocomplete="off" class="issue-checkbox-all tw-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}">
 		{{end}}
 		{{template "repo/issue/openclose" .}}
 		<!-- Total Tracked Time -->
diff --git a/templates/repo/issue/label_precolors.tmpl b/templates/repo/issue/label_precolors.tmpl
index 146119b978..80007662c0 100644
--- a/templates/repo/issue/label_precolors.tmpl
+++ b/templates/repo/issue/label_precolors.tmpl
@@ -1,5 +1,5 @@
 <div class="precolors">
-	<div class="gt-df">
+	<div class="tw-flex">
 		<a class="color" style="background-color:#e11d21" data-color-hex="#e11d21"></a>
 		<a class="color" style="background-color:#eb6420" data-color-hex="#eb6420"></a>
 		<a class="color" style="background-color:#fbca04" data-color-hex="#fbca04"></a>
@@ -9,7 +9,7 @@
 		<a class="color" style="background-color:#0052cc" data-color-hex="#0052cc"></a>
 		<a class="color" style="background-color:#5319e7" data-color-hex="#5319e7"></a>
 	</div>
-	<div class="gt-df">
+	<div class="tw-flex">
 		<a class="color" style="background-color:#f6c6c7" data-color-hex="#f6c6c7"></a>
 		<a class="color" style="background-color:#fad8c7" data-color-hex="#fad8c7"></a>
 		<a class="color" style="background-color:#fef2c0" data-color-hex="#fef2c0"></a>
diff --git a/templates/repo/issue/labels.tmpl b/templates/repo/issue/labels.tmpl
index 86e4bae0f7..230777efcc 100644
--- a/templates/repo/issue/labels.tmpl
+++ b/templates/repo/issue/labels.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository labels">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="navbar gt-mb-4">
+		<div class="issue-navbar tw-mb-4">
 			{{template "repo/issue/navbar" .}}
 			{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}}
 				<button class="ui small primary new-label button">{{ctx.Locale.Tr "repo.issues.new_label"}}</button>
diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl
index f41b4ee2c6..fcf69217ea 100644
--- a/templates/repo/issue/labels/edit_delete_label.tmpl
+++ b/templates/repo/issue/labels/edit_delete_label.tmpl
@@ -29,9 +29,9 @@
 					<label>{{ctx.Locale.Tr "repo.issues.label_exclusive"}}</label>
 				</div>
 				<br>
-				<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc" | Safe}}</small>
-				<div class="desc gt-ml-2 gt-mt-3 gt-hidden label-exclusive-warning">
-					{{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning" | Safe}}
+				<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small>
+				<div class="desc tw-ml-1 tw-mt-2 tw-hidden label-exclusive-warning">
+					{{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning"}}
 				</div>
 				<br>
 			</div>
@@ -40,7 +40,7 @@
 					<input class="label-is-archived-input" name="is_archived" type="checkbox">
 					<label>{{ctx.Locale.Tr "repo.issues.label_archive"}}</label>
 				</div>
-				<i class="gt-ml-2" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>
+				<i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>
 					{{svg "octicon-info"}}
 				</i>
 			</div>
@@ -52,8 +52,8 @@
 			</div>
 			<div class="field color-field">
 				<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label>
-				<div class="color picker column">
-					<input class="color-picker" name="color" value="#70c24a" required maxlength="7">
+				<div class="column js-color-picker-input">
+					<input name="color" value="#70c24a"placeholder="#c320f6" required maxlength="7">
 					{{template "repo/issue/label_precolors"}}
 				</div>
 			</div>
diff --git a/templates/repo/issue/labels/label.tmpl b/templates/repo/issue/labels/label.tmpl
index 3ecae09373..3651ba118f 100644
--- a/templates/repo/issue/labels/label.tmpl
+++ b/templates/repo/issue/labels/label.tmpl
@@ -1,7 +1,7 @@
 <a
-	class="item {{if not .label.IsChecked}}gt-hidden{{end}}"
+	class="item {{if not .label.IsChecked}}tw-hidden{{end}}"
 	id="label_{{.label.ID}}"
 	href="{{.root.RepoLink}}/{{if or .root.IsPull .root.Issue.IsPull}}pulls{{else}}issues{{end}}?labels={{.label.ID}}"{{/* FIXME: use .root.Issue.Link or create .root.Link */}}
 >
-	{{- RenderLabel $.Context .label -}}
+	{{- RenderLabel $.Context ctx.Locale .label -}}
 </a>
diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl
index 9a6065a407..8d7fc2c3db 100644
--- a/templates/repo/issue/labels/label_list.tmpl
+++ b/templates/repo/issue/labels/label_list.tmpl
@@ -3,16 +3,16 @@
 	<div class="ui right">
 		<div class="ui secondary menu">
 			<!-- Sort -->
-			<div class="item ui jump dropdown gt-py-3">
+			<div class="item ui jump dropdown tw-py-2">
 				<span class="text">
 					{{ctx.Locale.Tr "repo.issues.filter_sort"}}
 				</span>
 				{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-				<div class="left menu">
-					<a class="{{if or (eq .SortType "alphabetically") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
-					<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
-					<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="{{$.Link}}?sort=leastissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
-					<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="{{$.Link}}?sort=mostissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
+				<div class="menu">
+					<a class="{{if or (eq .SortType "alphabetically") (not .SortType)}}active {{end}}item" href="?sort=alphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
+					<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="?sort=reversealphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
+					<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?sort=leastissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
+					<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?sort=mostissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
 				</div>
 			</div>
 		</div>
@@ -32,7 +32,7 @@
 		{{range .Labels}}
 		<li class="item">
 			<div class="label-title">
-				{{RenderLabel $.Context .}}
+				{{RenderLabel $.Context ctx.Locale .}}
 				{{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}}
 			</div>
 			<div class="label-issues">
@@ -42,9 +42,9 @@
 					<a class="open-issues" href="{{$.RepoLink}}/issues?labels={{.ID}}">{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues.label_open_issues" .NumOpenIssues}}</a>
 				{{end}}
 			</div>
-			<div class="label-operation gt-df">
+			<div class="label-operation tw-flex">
 				{{template "repo/issue/labels/label_archived" .}}
-				<div class="gt-df gt-ml-auto">
+				<div class="tw-flex tw-ml-auto">
 					{{if and (not $.PageIsOrgSettingsLabels) (not $.Repository.IsArchived) (or $.CanWriteIssues $.CanWritePulls)}}
 						<a class="edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" {{if .Exclusive}}data-exclusive{{end}} {{if gt .ArchivedUnix 0}}data-is-archived{{end}} data-num-issues="{{.NumIssues}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.issues.label_edit"}}</a>
 						<a class="delete-button" href="#" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.label_delete"}}</a>
@@ -61,7 +61,7 @@
 			<li class="item">
 				<div class="ui grid middle aligned">
 					<div class="ten wide column">
-						{{ctx.Locale.Tr "repo.org_labels_desc" | Str2html}}
+						{{ctx.Locale.Tr "repo.org_labels_desc"}}
 						{{if .IsOrganizationOwner}}
 							<a href="{{.OrganizationLink}}/settings/labels">({{ctx.Locale.Tr "repo.org_labels_desc_manage"}})</a>:
 						{{end}}
@@ -72,7 +72,7 @@
 			{{range .OrgLabels}}
 			<li class="item org-label">
 				<div class="label-title">
-					{{RenderLabel $.Context .}}
+					{{RenderLabel $.Context ctx.Locale .}}
 					{{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}}
 				</div>
 				<div class="label-issues">
diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl
index e7fb1e5ff6..32fd8e76d7 100644
--- a/templates/repo/issue/labels/label_new.tmpl
+++ b/templates/repo/issue/labels/label_new.tmpl
@@ -17,7 +17,7 @@
 					<label>{{ctx.Locale.Tr "repo.issues.label_exclusive"}}</label>
 				</div>
 				<br>
-				<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc" | Safe}}</small>
+				<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small>
 			</div>
 			<div class="field">
 				<label for="description">{{ctx.Locale.Tr "repo.issues.label_description"}}</label>
@@ -27,8 +27,8 @@
 			</div>
 			<div class="field color-field">
 				<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label>
-				<div class="color picker column">
-					<input class="color-picker" name="color" value="#70c24a" required maxlength="7">
+				<div class="js-color-picker-input column">
+					<input name="color" value="#70c24a" placeholder="#c320f6" required maxlength="7">
 					{{template "repo/issue/label_precolors"}}
 				</div>
 			</div>
diff --git a/templates/repo/issue/labels/labels_selector_field.tmpl b/templates/repo/issue/labels/labels_selector_field.tmpl
index d24dac46eb..e5f15caca5 100644
--- a/templates/repo/issue/labels/labels_selector_field.tmpl
+++ b/templates/repo/issue/labels/labels_selector_field.tmpl
@@ -2,7 +2,7 @@
 	<span class="text muted flex-text-block">
 		<strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong>
 		{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
-			{{svg "octicon-gear" 16 "gt-ml-2"}}
+			{{svg "octicon-gear" 16 "tw-ml-1"}}
 		{{end}}
 	</span>
 	<div class="filter menu" {{if .Issue}}data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/labels"{{else}}data-id="#label_ids"{{end}}>
@@ -21,7 +21,7 @@
 					<div class="divider"></div>
 				{{end}}
 				{{$previousExclusiveScope = $exclusiveScope}}
-				<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}gt-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span>&nbsp;&nbsp;{{RenderLabel $.Context .}}
+				<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span>&nbsp;&nbsp;{{RenderLabel $.Context ctx.Locale .}}
 					{{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}}
 					<p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p>
 				</a>
@@ -34,7 +34,7 @@
 					<div class="divider"></div>
 				{{end}}
 				{{$previousExclusiveScope = $exclusiveScope}}
-				<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}gt-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span>&nbsp;&nbsp;{{RenderLabel $.Context .}}
+				<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span>&nbsp;&nbsp;{{RenderLabel $.Context ctx.Locale .}}
 					{{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}}
 					<p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p>
 				</a>
diff --git a/templates/repo/issue/labels/labels_sidebar.tmpl b/templates/repo/issue/labels/labels_sidebar.tmpl
index 4f41054a91..be30baba92 100644
--- a/templates/repo/issue/labels/labels_sidebar.tmpl
+++ b/templates/repo/issue/labels/labels_sidebar.tmpl
@@ -1,5 +1,5 @@
 <div class="ui labels list">
-	<span class="no-select item {{if .root.HasSelectedLabel}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
+	<span class="no-select item {{if .root.HasSelectedLabel}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
 	<span class="labels-list">
 		{{range .root.Labels}}
 			{{template "repo/issue/labels/label" dict "root" $.root "label" .}}
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index 012b613fbf..30edf825f1 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -2,11 +2,12 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository issue-list">
 	{{template "repo/header" .}}
 	<div class="ui container">
+	{{template "base/alert" .}}
 
 	{{if .PinnedIssues}}
 		<div id="issue-pins" {{if .IsRepoAdmin}}data-is-repo-admin{{end}}>
 			{{range .PinnedIssues}}
-				<div class="issue-card gt-word-break {{if $.IsRepoAdmin}}gt-cursor-grab{{end}}" data-move-url="{{$.Link}}/move_pin" data-issue-id="{{.ID}}">
+				<div class="issue-card gt-word-break {{if $.IsRepoAdmin}}tw-cursor-grab{{end}}" data-move-url="{{$.Link}}/move_pin" data-issue-id="{{.ID}}">
 					{{template "repo/issue/card" (dict "Issue" . "Page" $ "isPinnedIssueCard" true)}}
 				</div>
 			{{end}}
@@ -31,7 +32,7 @@
 
 		{{template "repo/issue/filters" .}}
 
-		<div id="issue-actions" class="issue-list-toolbar gt-hidden">
+		<div id="issue-actions" class="issue-list-toolbar tw-hidden">
 			<div class="issue-list-toolbar-left">
 				{{template "repo/issue/openclose" .}}
 				<!-- Total Tracked Time -->
diff --git a/templates/repo/issue/milestone/filter_list.tmpl b/templates/repo/issue/milestone/filter_list.tmpl
index 0eea42d6ee..45f9866a16 100644
--- a/templates/repo/issue/milestone/filter_list.tmpl
+++ b/templates/repo/issue/milestone/filter_list.tmpl
@@ -5,11 +5,11 @@
 	</span>
 	{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 	<div class="menu">
-		<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a>
-		<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="{{$.Link}}?sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a>
-		<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="{{$.Link}}?sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
-		<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="{{$.Link}}?sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
-		<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="{{$.Link}}?sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
-		<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="{{$.Link}}?sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
+		<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="?sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a>
+		<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="?sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a>
+		<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="?sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
+		<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
+		<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
+		<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
 	</div>
 </div>
diff --git a/templates/repo/issue/milestone/select_menu.tmpl b/templates/repo/issue/milestone/select_menu.tmpl
index 6f8c6c85c2..9b0492ce52 100644
--- a/templates/repo/issue/milestone/select_menu.tmpl
+++ b/templates/repo/issue/milestone/select_menu.tmpl
@@ -18,7 +18,7 @@
 		</div>
 		{{range .OpenMilestones}}
 			<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
-				{{svg "octicon-milestone" 16 "gt-mr-2"}}
+				{{svg "octicon-milestone" 16 "tw-mr-1"}}
 				{{.Name}}
 			</a>
 		{{end}}
@@ -30,7 +30,7 @@
 		</div>
 		{{range .ClosedMilestones}}
 			<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
-				{{svg "octicon-milestone" 16 "gt-mr-2"}}
+				{{svg "octicon-milestone" 16 "tw-mr-1"}}
 				{{.Name}}
 			</a>
 		{{end}}
diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl
index ea19518efa..5bae6fc6d5 100644
--- a/templates/repo/issue/milestone_issues.tmpl
+++ b/templates/repo/issue/milestone_issues.tmpl
@@ -2,10 +2,11 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository milestone-issue-list">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="gt-df">
-			<h1 class="gt-mb-3">{{.Milestone.Name}}</h1>
+		{{template "base/alert" .}}
+		<div class="tw-flex">
+			<h1 class="tw-mb-2">{{.Milestone.Name}}</h1>
 			{{if not .Repository.IsArchived}}
-				<div class="text right gt-f1">
+				<div class="text right tw-flex-1">
 					{{if or .CanWriteIssues .CanWritePulls}}
 						{{if .Milestone.IsClosed}}
 							<a class="ui primary basic button link-action" href data-url="{{$.RepoLink}}/milestones/{{.MilestoneID}}/open">{{ctx.Locale.Tr "repo.milestones.open"}}
@@ -21,17 +22,17 @@
 			{{end}}
 		</div>
 		{{if .Milestone.RenderedContent}}
-		<div class="markup content gt-mb-4">
-				{{.Milestone.RenderedContent|Str2html}}
+		<div class="markup content tw-mb-4">
+				{{.Milestone.RenderedContent}}
 		</div>
 		{{end}}
-		<div class="gt-df gt-fc gt-gap-3">
+		<div class="tw-flex tw-flex-col tw-gap-2">
 			<progress class="milestone-progress-big" value="{{.Milestone.Completeness}}" max="100"></progress>
-			<div class="gt-df gt-gap-4">
-				<div classs="gt-df gt-ac">
+			<div class="tw-flex tw-gap-4">
+				<div classs="tw-flex tw-items-center">
 					{{$closedDate:= TimeSinceUnix .Milestone.ClosedDateUnix ctx.Locale}}
 					{{if .IsClosed}}
-						{{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.closed" $closedDate | Safe}}
+						{{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.closed" $closedDate}}
 					{{else}}
 
 						{{if .Milestone.DeadlineString}}
@@ -45,7 +46,7 @@
 						{{end}}
 					{{end}}
 				</div>
-				<div class="gt-mr-3">{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness | Safe}}</div>
+				<div class="tw-mr-2">{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness}}</div>
 				{{if .TotalTrackedTime}}
 					<div data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'>
 						{{svg "octicon-clock"}}
diff --git a/templates/repo/issue/milestone_new.tmpl b/templates/repo/issue/milestone_new.tmpl
index 3e79ee7ee9..9f32df00e3 100644
--- a/templates/repo/issue/milestone_new.tmpl
+++ b/templates/repo/issue/milestone_new.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository new milestone">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="navbar">
+		<div class="issue-navbar">
 			{{template "repo/issue/navbar" .}}
 			{{if and (or .CanWriteIssues .CanWritePulls) .PageIsEditMilestone}}
 				<div class="ui right floated secondary menu">
@@ -39,7 +39,7 @@
 					<textarea name="content">{{.content}}</textarea>
 				</div>
 				<div class="divider"></div>
-				<div class="gt-text-right">
+				<div class="tw-text-right">
 					{{if .PageIsEditMilestone}}
 						<a class="ui primary basic button" href="{{.RepoLink}}/milestones">
 							{{ctx.Locale.Tr "repo.milestones.cancel"}}
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl
index 3d4bbfd8b1..bce7ad8717 100644
--- a/templates/repo/issue/milestones.tmpl
+++ b/templates/repo/issue/milestones.tmpl
@@ -19,12 +19,12 @@
 			{{range .Milestones}}
 				<li class="milestone-card">
 					<div class="milestone-header">
-						<h3 class="flex-text-block gt-m-0">
+						<h3 class="flex-text-block tw-m-0">
 							{{svg "octicon-milestone" 16}}
 							<a class="muted" href="{{$.RepoLink}}/milestone/{{.ID}}">{{.Name}}</a>
 						</h3>
-						<div class="gt-df gt-ac">
-							<span class="gt-mr-3">{{.Completeness}}%</span>
+						<div class="tw-flex tw-items-center">
+							<span class="tw-mr-2">{{.Completeness}}%</span>
 							<progress value="{{.Completeness}}" max="100"></progress>
 						</div>
 					</div>
@@ -47,14 +47,14 @@
 							{{if .UpdatedUnix}}
 								<div class="flex-text-block">
 									{{svg "octicon-clock"}}
-									{{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale) | Safe}}
+									{{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale)}}
 								</div>
 							{{end}}
 							<div class="flex-text-block">
 								{{if .IsClosed}}
 									{{$closedDate:= TimeSinceUnix .ClosedDateUnix ctx.Locale}}
 									{{svg "octicon-clock" 14}}
-									{{ctx.Locale.Tr "repo.milestones.closed" $closedDate | Safe}}
+									{{ctx.Locale.Tr "repo.milestones.closed" $closedDate}}
 								{{else}}
 									{{if .DeadlineString}}
 										<span class="flex-text-inline {{if .IsOverdue}}text red{{end}}">
@@ -82,7 +82,7 @@
 					</div>
 					{{if .Content}}
 						<div class="markup content">
-							{{.RenderedContent|Str2html}}
+							{{.RenderedContent}}
 						</div>
 					{{end}}
 				</li>
diff --git a/templates/repo/issue/navbar.tmpl b/templates/repo/issue/navbar.tmpl
index 16110597ed..30e42c77cc 100644
--- a/templates/repo/issue/navbar.tmpl
+++ b/templates/repo/issue/navbar.tmpl
@@ -1,4 +1,4 @@
-<h2 class="ui compact small menu header small-menu-items issue-list-navbar">
+<h2 class="ui compact small menu small-menu-items issue-list-navbar">
 	<a class="{{if .PageIsLabels}}active {{end}}item" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
 	<a class="{{if .PageIsMilestones}}active {{end}}item" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
 </h2>
diff --git a/templates/repo/issue/new.tmpl b/templates/repo/issue/new.tmpl
index 780e874bc6..ccd45fdebe 100644
--- a/templates/repo/issue/new.tmpl
+++ b/templates/repo/issue/new.tmpl
@@ -2,14 +2,6 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository new issue">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		{{if .Flash.WarningMsg}}
-			{{/*
-			There's already an importing of alert.tmpl in new_form.tmpl,
-			but only the negative message will be displayed within forms for some reasons, see semantic.css:10659.
-			To avoid repeated negative messages, the importing here if for .Flash.WarningMsg only.
-			 */}}
-			{{template "base/alert" .}}
-		{{end}}
 		{{template "repo/issue/new_form" .}}
 	</div>
 </div>
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index 04ae8456bb..88a6c39e52 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -1,19 +1,17 @@
+{{if .Flash}}
+{{template "base/alert" .}}
+{{end}}
 <form class="issue-content ui comment form form-fetch-action" id="new-issue" action="{{.Link}}" method="post">
 	{{.CsrfTokenHtml}}
-	{{if .Flash}}
-		<div class="sixteen wide column">
-			{{template "base/alert" .}}
-		</div>
-	{{end}}
 	<div class="issue-content-left">
 		<div class="ui comments">
 			<div class="comment">
 				{{ctx.AvatarUtils.Avatar .SignedUser 40}}
-				<div class="ui segment content gt-my-0">
+				<div class="ui segment content tw-my-0">
 					<div class="field">
-						<input name="title" id="issue_title" placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" autofocus required maxlength="255" autocomplete="off">
+						<input name="title" class="js-autofocus-end" id="issue_title" placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" required maxlength="255" autocomplete="off">
 						{{if .PageIsComparePull}}
-							<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}</div>
+							<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0)}}</div>
 						{{end}}
 					</div>
 					{{if .Fields}}
@@ -62,7 +60,7 @@
 			<span class="text flex-text-block">
 				<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong>
 				{{if .HasIssuesOrPullsWritePermission}}
-					{{svg "octicon-gear" 16 "gt-ml-2"}}
+					{{svg "octicon-gear" 16 "tw-ml-1"}}
 				{{end}}
 			</span>
 			<div class="menu">
@@ -70,11 +68,11 @@
 			</div>
 		</div>
 		<div class="ui select-milestone list">
-			<span class="no-select item {{if .Milestone}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
+			<span class="no-select item {{if .Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
 			<div class="selected">
 				{{if .Milestone}}
 					<a class="item muted sidebar-item-link" href="{{.RepoLink}}/issues?milestone={{.Milestone.ID}}">
-						{{svg "octicon-milestone" 18 "gt-mr-3"}}
+						{{svg "octicon-milestone" 18 "tw-mr-2"}}
 						{{.Milestone.Name}}
 					</a>
 				{{end}}
@@ -89,7 +87,7 @@
 			<span class="text flex-text-block">
 				<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong>
 				{{if .HasIssuesOrPullsWritePermission}}
-					{{svg "octicon-gear" 16 "gt-ml-2"}}
+					{{svg "octicon-gear" 16 "tw-ml-1"}}
 				{{end}}
 			</span>
 			<div class="menu">
@@ -112,7 +110,7 @@
 						</div>
 						{{range .OpenProjects}}
 							<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
-								{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+								{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 							</a>
 						{{end}}
 					{{end}}
@@ -123,7 +121,7 @@
 						</div>
 						{{range .ClosedProjects}}
 							<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
-								{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+								{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 							</a>
 						{{end}}
 					{{end}}
@@ -131,11 +129,11 @@
 			</div>
 		</div>
 		<div class="ui select-project list">
-			<span class="no-select item {{if .Project}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
+			<span class="no-select item {{if .Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
 			<div class="selected">
 				{{if .Project}}
 					<a class="item muted sidebar-item-link" href="{{.Project.Link ctx}}">
-						{{svg .Project.IconName 18 "gt-mr-3"}}{{.Project.Title}}
+						{{svg .Project.IconName 18 "tw-mr-2"}}{{.Project.Title}}
 					</a>
 				{{end}}
 			</div>
@@ -147,7 +145,7 @@
 				<span class="text flex-text-block">
 					<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong>
 					{{if .HasIssuesOrPullsWritePermission}}
-						{{svg "octicon-gear" 16 "gt-ml-2"}}
+						{{svg "octicon-gear" 16 "tw-ml-1"}}
 					{{end}}
 				</span>
 				<div class="filter menu" data-id="#assignee_ids">
@@ -158,22 +156,22 @@
 					<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
 					{{range .Assignees}}
 						<a class="item muted" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
-							<span class="octicon-check gt-invisible">{{svg "octicon-check"}}</span>
+							<span class="octicon-check tw-invisible">{{svg "octicon-check"}}</span>
 							<span class="text">
-								{{ctx.AvatarUtils.Avatar . 28 "gt-mr-3"}}{{template "repo/search_name" .}}
+								{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}{{template "repo/search_name" .}}
 							</span>
 						</a>
 					{{end}}
 				</div>
 			</div>
 			<div class="ui assignees list">
-				<span class="no-select item {{if .HasSelectedLabel}}gt-hidden{{end}}">
+				<span class="no-select item {{if .HasSelectedLabel}}tw-hidden{{end}}">
 					{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}
 				</span>
 				<div class="selected">
 				{{range .Assignees}}
-					<a class="item gt-p-2 muted gt-hidden" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}">
-						{{ctx.AvatarUtils.Avatar . 28 "gt-mr-3 gt-vm"}}{{.GetDisplayName}}
+					<a class="item tw-p-1 muted tw-hidden" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}">
+						{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2 tw-align-middle"}}{{.GetDisplayName}}
 					</a>
 				{{end}}
 				</div>
diff --git a/templates/repo/issue/openclose.tmpl b/templates/repo/issue/openclose.tmpl
index 38848c51ac..eb2d6e09ee 100644
--- a/templates/repo/issue/openclose.tmpl
+++ b/templates/repo/issue/openclose.tmpl
@@ -1,16 +1,16 @@
 <div class="small-menu-items ui compact tiny menu">
 	<a class="{{if eq .State "open"}}active {{end}}item" href="{{if eq .State "open"}}{{.AllStatesLink}}{{else}}{{.OpenLink}}{{end}}">
 		{{if .PageIsMilestones}}
-			{{svg "octicon-milestone" 16 "gt-mr-3"}}
+			{{svg "octicon-milestone" 16 "tw-mr-2"}}
 		{{else if .PageIsPullList}}
-			{{svg "octicon-git-pull-request" 16 "gt-mr-3"}}
+			{{svg "octicon-git-pull-request" 16 "tw-mr-2"}}
 		{{else}}
-			{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
+			{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
 		{{end}}
 		{{ctx.Locale.PrettyNumber .OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 	</a>
 	<a class="{{if eq .State "closed"}}active {{end}}item" href="{{if eq .State "closed"}}{{.AllStatesLink}}{{else}}{{.ClosedLink}}{{end}}">
-		{{svg "octicon-check" 16 "gt-mr-3"}}
+		{{svg "octicon-check" 16 "tw-mr-2"}}
 		{{ctx.Locale.PrettyNumber .ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 	</a>
 </div>
diff --git a/templates/repo/issue/search.tmpl b/templates/repo/issue/search.tmpl
index 361f16fd3b..769387b51c 100644
--- a/templates/repo/issue/search.tmpl
+++ b/templates/repo/issue/search.tmpl
@@ -9,10 +9,10 @@
 			<input type="hidden" name="assignee" value="{{$.AssigneeID}}">
 			<input type="hidden" name="poster" value="{{$.PosterID}}">
 		{{end}}
-		{{template "shared/searchinput" dict "Value" .Keyword}}
+		{{template "shared/search/input" dict "Value" .Keyword}}
 		{{if .PageIsIssueList}}
-			<button id="issue-list-quick-goto" class="ui small icon button gt-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}" data-repo-link="{{.RepoLink}}">{{svg "octicon-hash"}}</button>
+			<button id="issue-list-quick-goto" class="ui small icon button tw-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}" data-repo-link="{{.RepoLink}}">{{svg "octicon-hash"}}</button>
 		{{end}}
-		<button class="ui small icon button" aria-label="{{ctx.Locale.Tr "explore.search"}}">{{svg "octicon-search"}}</button>
+		{{template "shared/search/button"}}
 	</div>
 </form>
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index ed444f6dce..c65b79dea7 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -20,18 +20,18 @@
 				</a>
 				{{end}}
 				<div class="content comment-container">
-					<div class="ui top attached header comment-header gt-df gt-ac gt-sb" role="heading" aria-level="3">
-						<div class="comment-header-left gt-df gt-ac">
+					<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between" role="heading" aria-level="3">
+						<div class="comment-header-left tw-flex tw-items-center">
 							{{if .Issue.OriginalAuthor}}
-								<span class="text black gt-font-semibold">
+								<span class="text black tw-font-semibold">
 									{{svg (MigrationIcon .Repository.GetOriginalURLHostname)}}
 									{{.Issue.OriginalAuthor}}
 								</span>
 								<span class="text grey muted-links">
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr | Safe}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr}}
 								</span>
 								<span class="text migrate">
-									{{if .Repository.OriginalURL}} ({{ctx.Locale.Tr "repo.migrated_from" (.Repository.OriginalURL|Escape) (.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}
+									{{if .Repository.OriginalURL}} ({{ctx.Locale.Tr "repo.migrated_from" .Repository.OriginalURL .Repository.GetOriginalURLHostname}}){{end}}
 								</span>
 							{{else}}
 								<a class="inline-timeline-avatar" href="{{.Issue.Poster.HomeLink}}">
@@ -39,11 +39,11 @@
 								</a>
 								<span class="text grey muted-links">
 									{{template "shared/user/authorlink" .Issue.Poster}}
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr | Safe}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr}}
 								</span>
 							{{end}}
 						</div>
-						<div class="comment-header-right actions gt-df gt-ac">
+						<div class="comment-header-right actions tw-flex tw-items-center">
 							{{template "repo/issue/view_content/show_role" dict "ShowRole" .Issue.ShowRole "IgnorePoster" true}}
 							{{if not $.Repository.IsArchived}}
 								{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
@@ -54,15 +54,15 @@
 					<div class="ui attached segment comment-body" role="article">
 						<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission $.IsIssuePoster}}data-can-edit="true"{{end}}>
 							{{if .Issue.RenderedContent}}
-								{{.Issue.RenderedContent|Str2html}}
+								{{.Issue.RenderedContent}}
 							{{else}}
 								<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 							{{end}}
 						</div>
-						<div id="issue-{{.Issue.ID}}-raw" class="raw-content gt-hidden">{{.Issue.Content}}</div>
-						<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}" data-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/attachments" data-view-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/view-attachments"></div>
+						<div id="issue-{{.Issue.ID}}-raw" class="raw-content tw-hidden">{{.Issue.Content}}</div>
+						<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}" data-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/attachments" data-view-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/view-attachments"></div>
 						{{if .Issue.Attachments}}
-							{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Issue.Attachments "Content" .Issue.RenderedContent}}
+							{{template "repo/issue/view_content/attachments" dict "Attachments" .Issue.Attachments "RenderedContent" .Issue.RenderedContent}}
 						{{end}}
 					</div>
 					{{$reactions := .Issue.Reactions.GroupByType}}
@@ -114,7 +114,7 @@
 					</div>
 				</div>
 				{{else if .Repository.IsArchived}}
-					<div class="ui warning message gt-text-center">
+					<div class="ui warning message tw-text-center">
 						{{if .Issue.IsPull}}
 							{{ctx.Locale.Tr "repo.archive.pull.nocomment"}}
 						{{else}}
@@ -124,7 +124,7 @@
 				{{end}}
 			{{else}} {{/* not .IsSigned */}}
 				{{if .Repository.IsArchived}}
-					<div class="ui warning message gt-text-center">
+					<div class="ui warning message tw-text-center">
 						{{if .Issue.IsPull}}
 							{{ctx.Locale.Tr "repo.archive.pull.nocomment"}}
 						{{else}}
@@ -133,7 +133,7 @@
 					</div>
 				{{else}}
 					<div class="ui warning message">
-						{{ctx.Locale.Tr "repo.issues.sign_in_require_desc" (.SignInLink|Escape) | Safe}}
+						{{ctx.Locale.Tr "repo.issues.sign_in_require_desc" .SignInLink}}
 					</div>
 				{{end}}
 			{{end}}{{/* end if: .IsSigned */}}
@@ -170,8 +170,9 @@
 </template>
 
 {{template "repo/issue/view_content/reference_issue_dialog" .}}
+{{template "shared/user/block_user_dialog" .}}
 
-<div class="gt-hidden" id="no-content">
+<div class="tw-hidden" id="no-content">
 	<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 </div>
 
@@ -181,7 +182,7 @@
 		{{ctx.Locale.Tr "repo.branch.delete" .HeadTarget}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "repo.branch.delete_desc" | Str2html}}</p>
+		<p>{{ctx.Locale.Tr "repo.branch.delete_desc"}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl
index 1fb6f2f2c2..2155f78656 100644
--- a/templates/repo/issue/view_content/attachments.tmpl
+++ b/templates/repo/issue/view_content/attachments.tmpl
@@ -4,11 +4,11 @@
 	{{end}}
 	{{$hasThumbnails := false}}
 	{{- range .Attachments -}}
-		<div class="gt-df">
-			<div class="gt-f1 gt-p-3">
+		<div class="tw-flex">
+			<div class="tw-flex-1 tw-p-2">
 				<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}">
 					{{if FilenameIsImage .Name}}
-						{{if not (StringUtils.Contains $.Content .UUID)}}
+						{{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}}
 							{{$hasThumbnails = true}}
 						{{end}}
 						{{svg "octicon-file"}}
@@ -18,7 +18,7 @@
 					<span><strong>{{.Name}}</strong></span>
 				</a>
 			</div>
-			<div class="gt-p-3 gt-df gt-ac">
+			<div class="tw-p-2 tw-flex tw-items-center">
 				<span class="ui text grey">{{.Size | FileSize}}</span>
 			</div>
 		</div>
@@ -29,7 +29,7 @@
 		<div class="ui small thumbnails">
 			{{- range .Attachments -}}
 				{{if FilenameIsImage .Name}}
-					{{if not (StringUtils.Contains $.Content .UUID)}}
+					{{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}}
 					<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}">
 						<img alt="{{.Name}}" src="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}">
 					</a>
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index ade0ea34cf..f65dc6ee90 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -25,18 +25,18 @@
 				</a>
 			{{end}}
 				<div class="content comment-container">
-					<div class="ui top attached header comment-header gt-df gt-ac gt-sb" role="heading" aria-level="3">
-						<div class="comment-header-left gt-df gt-ac">
+					<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between" role="heading" aria-level="3">
+						<div class="comment-header-left tw-flex tw-items-center">
 							{{if .OriginalAuthor}}
-								<span class="text black gt-font-semibold gt-mr-2">
+								<span class="text black tw-font-semibold tw-mr-1">
 									{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
 									{{.OriginalAuthor}}
 								</span>
 								<span class="text grey muted-links">
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} {{if $.Repository.OriginalURL}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}} {{if $.Repository.OriginalURL}}
 								</span>
 								<span class="text migrate">
-									({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}
+									({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}}){{end}}
 								</span>
 							{{else}}
 								{{if gt .Poster.ID 0}}
@@ -46,11 +46,11 @@
 								{{end}}
 								<span class="text grey muted-links">
 									{{template "shared/user/authorlink" .Poster}}
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}}
+									{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}}
 								</span>
 							{{end}}
 						</div>
-						<div class="comment-header-right actions gt-df gt-ac">
+						<div class="comment-header-right actions tw-flex tw-items-center">
 							{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
 							{{if not $.Repository.IsArchived}}
 								{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
@@ -61,15 +61,15 @@
 					<div class="ui attached segment comment-body" role="article">
 						<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}>
 							{{if .RenderedContent}}
-								{{.RenderedContent|Str2html}}
+								{{.RenderedContent}}
 							{{else}}
 								<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 							{{end}}
 						</div>
-						<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
-						<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
+						<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
+						<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
 						{{if .Attachments}}
-							{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}}
+							{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 						{{end}}
 					</div>
 					{{$reactions := .Reactions.GroupByType}}
@@ -80,48 +80,54 @@
 			</div>
 		{{else if eq .Type 1}}
 			<div class="timeline-item event" id="{{.HashTag}}">
-				<span class="badge gt-bg-green gt-text-white">{{svg "octicon-dot-fill"}}</span>
-				{{template "shared/user/avatarlink" dict "user" .Poster}}
+				<span class="badge tw-bg-green tw-text-white">{{svg "octicon-dot-fill"}}</span>
+				{{if not .OriginalAuthor}}
+					{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{end}}
 				<span class="text grey muted-links">
-					{{template "shared/user/authorlink" .Poster}}
+					{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
 					{{if .Issue.IsPull}}
-						{{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr}}
 					{{end}}
 				</span>
 			</div>
 		{{else if eq .Type 2}}
 			<div class="timeline-item event" id="{{.HashTag}}">
-				<span class="badge gt-bg-red gt-text-white">{{svg "octicon-circle-slash"}}</span>
-				{{template "shared/user/avatarlink" dict "user" .Poster}}
+				<span class="badge tw-bg-red tw-text-white">{{svg "octicon-circle-slash"}}</span>
+				{{if not .OriginalAuthor}}
+					{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{end}}
 				<span class="text grey muted-links">
-					{{template "shared/user/authorlink" .Poster}}
+					{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
 					{{if .Issue.IsPull}}
-						{{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr}}
 					{{end}}
 				</span>
 			</div>
 		{{else if eq .Type 28}}
 			<div class="timeline-item event" id="{{.HashTag}}">
-				<span class="badge gt-bg-purple gt-text-white">{{svg "octicon-git-merge"}}</span>
-				{{template "shared/user/avatarlink" dict "user" .Poster}}
+				<span class="badge tw-bg-purple tw-text-white">{{svg "octicon-git-merge"}}</span>
+				{{if not .OriginalAuthor}}
+					{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{end}}
 				<span class="text grey muted-links">
-					{{template "shared/user/authorlink" .Poster}}
+					{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
 					{{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}}
 					{{if eq $.Issue.PullRequest.Status 3}}
-						{{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape)) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (HTMLFormat `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` $link (ShortSha $.Issue.PullRequest.MergedCommitID)) (HTMLFormat "<b>%[1]s</b>" $.BaseTarget) $createdStr}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape)) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (HTMLFormat `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` $link (ShortSha $.Issue.PullRequest.MergedCommitID)) (HTMLFormat "<b>%[1]s</b>" $.BaseTarget) $createdStr}}
 					{{end}}
 				</span>
 			</div>
 		{{else if eq .Type 3 5 6}}
 			{{$refFrom:= ""}}
 			{{if ne .RefRepoID .Issue.RepoID}}
-				{{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" (.RefRepo.FullName|Escape)}}
+				{{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" .RefRepo.FullName}}
 			{{end}}
 			{{$refTr := "repo.issues.ref_issue_from"}}
 			{{if .Issue.IsPull}}
@@ -138,11 +144,11 @@
 				{{if eq .RefAction 3}}<del>{{end}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom | Safe}}
+					{{ctx.Locale.Tr $refTr .EventTag $createdStr (.RefCommentLink ctx) $refFrom}}
 				</span>
 				{{if eq .RefAction 3}}</del>{{end}}
 
-				<div class="detail">
+				<div class="detail flex-text-block">
 					<span class="text grey muted-links"><a href="{{.RefIssueLink ctx}}"><b>{{.RefIssueTitle ctx}}</b> {{.RefIssueIdent ctx}}</a></span>
 				</div>
 			</div>
@@ -152,11 +158,11 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr}}
 				</span>
-				<div class="detail">
+				<div class="detail flex-text-block">
 					{{svg "octicon-git-commit"}}
-					<span class="text grey muted-links">{{.Content | Str2html}}</span>
+					<span class="text grey muted-links">{{.Content | SanitizeHTML}}</span>
 				</div>
 			</div>
 		{{else if eq .Type 7}}
@@ -167,11 +173,11 @@
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Poster}}
 						{{if and .AddedLabels (not .RemovedLabels)}}
-							{{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr | Safe}}
+							{{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context ctx.Locale .AddedLabels $.RepoLink) $createdStr}}
 						{{else if and (not .AddedLabels) .RemovedLabels}}
-							{{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}}
+							{{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context ctx.Locale .RemovedLabels $.RepoLink) $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context ctx.Locale .AddedLabels $.RepoLink) (RenderLabels $.Context ctx.Locale .RemovedLabels $.RepoLink) $createdStr}}
 						{{end}}
 					</span>
 				</div>
@@ -182,7 +188,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr | Safe}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr | Safe}}{{end}}
+					{{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" .OldMilestone.Name .Milestone.Name $createdStr}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" .OldMilestone.Name $createdStr}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" .Milestone.Name $createdStr}}{{end}}
 				</span>
 			</div>
 		{{else if and (eq .Type 9) (gt .AssigneeID 0)}}
@@ -193,9 +199,9 @@
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Assignee}}
 						{{if eq .Poster.ID .Assignee.ID}}
-							{{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.remove_assignee_at" .Poster.GetDisplayName $createdStr}}
 						{{end}}
 					</span>
 				{{else}}
@@ -203,9 +209,9 @@
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Assignee}}
 						{{if eq .Poster.ID .AssigneeID}}
-							{{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.add_assignee_at" .Poster.GetDisplayName $createdStr}}
 						{{end}}
 					</span>
 				{{end}}
@@ -216,7 +222,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 11}}
@@ -225,7 +231,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.delete_branch_at" .OldRef $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 12}}
@@ -234,7 +240,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 13}}
@@ -243,10 +249,10 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr}}
 				</span>
 				{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
-				<div class="detail">
+				<div class="detail flex-text-block">
 					{{svg "octicon-clock"}}
 					{{if .RenderedContent}}
 						{{/* compatibility with time comments made before v1.21 */}}
@@ -262,10 +268,10 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr}}
 				</span>
 				{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
-				<div class="detail">
+				<div class="detail flex-text-block">
 					{{svg "octicon-clock"}}
 					{{if .RenderedContent}}
 						{{/* compatibility with time comments made before v1.21 */}}
@@ -281,7 +287,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 16}}
@@ -290,7 +296,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 17}}
@@ -303,7 +309,7 @@
 					{{if eq (len $parsedDeadline) 2}}
 						{{$from := DateTime "long" (index $parsedDeadline 1)}}
 						{{$to := DateTime "long" (index $parsedDeadline 0)}}
-						{{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr}}
 					{{end}}
 				</span>
 			</div>
@@ -313,7 +319,7 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 19}}
@@ -322,10 +328,10 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr}}
 				</span>
 				{{if .DependentIssue}}
-					<div class="detail">
+					<div class="detail flex-text-block">
 						{{svg "octicon-plus"}}
 						<span class="text grey muted-links">
 							<a href="{{.DependentIssue.Link}}">
@@ -345,11 +351,11 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr}}
 				</span>
 				{{if .DependentIssue}}
-					<div class="detail">
-						<span class="text grey muted-links">{{svg "octicon-trash"}}</span>
+					<div class="detail flex-text-block">
+						{{svg "octicon-trash"}}
 						<span class="text grey muted-links">
 							<a href="{{.DependentIssue.Link}}">
 								{{if eq .DependentIssue.RepoID .Issue.RepoID}}
@@ -365,38 +371,30 @@
 		{{else if eq .Type 22}}
 			<div class="timeline-item-group" id="{{.HashTag}}">
 				<div class="timeline-item event">
-					{{if .OriginalAuthor}}
-					{{else}}
-					{{/* Some timeline avatars need a offset to correctly allign with their speech
-							bubble. The condition depends on review type and for positive reviews whether
-							there is a comment element or not */}}
-					<a class="timeline-avatar{{if or (and (eq .Review.Type 1) (or .Content .Attachments)) (and (eq .Review.Type 2) (or .Content .Attachments)) (eq .Review.Type 3)}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
+					{{$reviewType := -1}}
+					{{if .Review}}{{$reviewType = .Review.Type}}{{end}}
+					{{if not .OriginalAuthor}}
+					{{/* Some timeline avatars need a offset to correctly align with their speech bubble.
+						The condition depends on whether the comment has contents/attachments or reviews */}}
+					<a class="timeline-avatar{{if or .Content .Attachments (and .Review .Review.CodeComments)}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
 						{{ctx.AvatarUtils.Avatar .Poster 40}}
 					</a>
 					{{end}}
-					<span class="badge{{if eq .Review.Type 1}} gt-bg-green gt-text-white{{else if eq .Review.Type 3}} gt-bg-red gt-text-white{{end}}">{{svg (printf "octicon-%s" .Review.Type.Icon)}}</span>
+					<span class="badge{{if eq $reviewType 1}} tw-bg-green tw-text-white{{else if eq $reviewType 3}} tw-bg-red tw-text-white{{end}}">
+						{{if .Review}}{{svg (printf "octicon-%s" .Review.Type.Icon)}}{{end}}
+					</span>
 					<span class="text grey muted-links">
-						{{if .OriginalAuthor}}
-							<span class="text black">
-								{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
-								{{.OriginalAuthor}}
-							</span>
-							<span class="text grey muted-links"> {{if $.Repository.OriginalURL}}</span>
-							<span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}</span>
+						{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
+						{{if eq $reviewType 1}}
+							{{ctx.Locale.Tr "repo.issues.review.approve" $createdStr}}
+						{{else if eq $reviewType 2}}
+							{{ctx.Locale.Tr "repo.issues.review.comment" $createdStr}}
+						{{else if eq $reviewType 3}}
+							{{ctx.Locale.Tr "repo.issues.review.reject" $createdStr}}
 						{{else}}
-							{{template "shared/user/authorlink" .Poster}}
+							{{ctx.Locale.Tr "repo.issues.review.comment" $createdStr}}
 						{{end}}
-
-						{{if eq .Review.Type 1}}
-							{{ctx.Locale.Tr "repo.issues.review.approve" $createdStr | Safe}}
-						{{else if eq .Review.Type 2}}
-							{{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}}
-						{{else if eq .Review.Type 3}}
-							{{ctx.Locale.Tr "repo.issues.review.reject" $createdStr | Safe}}
-						{{else}}
-							{{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}}
-						{{end}}
-						{{if .Review.Dismissed}}
+						{{if and .Review .Review.Dismissed}}
 							<div class="ui small label">{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}</div>
 						{{end}}
 					</span>
@@ -404,8 +402,8 @@
 				{{if or .Content .Attachments}}
 				<div class="timeline-item comment">
 					<div class="content comment-container">
-						<div class="ui top attached header comment-header gt-df gt-ac gt-sb">
-							<div class="comment-header-left gt-df gt-ac">
+						<div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between">
+							<div class="comment-header-left tw-flex tw-items-center">
 								{{if gt .Poster.ID 0}}
 									<a class="inline-timeline-avatar" href="{{.Poster.HomeLink}}">
 										{{ctx.AvatarUtils.Avatar .Poster 24}}
@@ -413,20 +411,20 @@
 								{{end}}
 								<span class="text grey muted-links">
 									{{if .OriginalAuthor}}
-										<span class="text black gt-font-semibold">
+										<span class="text black tw-font-semibold">
 											{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
 											{{.OriginalAuthor}}
 										</span>
 										<span class="text grey muted-links"> {{if $.Repository.OriginalURL}}</span>
-										<span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}</span>
+										<span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}}){{end}}</span>
 									{{else}}
 										{{template "shared/user/authorlink" .Poster}}
 									{{end}}
 
-									{{ctx.Locale.Tr "repo.issues.review.left_comment" | Safe}}
+									{{ctx.Locale.Tr "repo.issues.review.left_comment"}}
 								</span>
 							</div>
-							<div class="comment-header-right actions gt-df gt-ac">
+							<div class="comment-header-right actions tw-flex tw-items-center">
 								{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
 								{{if not $.Repository.IsArchived}}
 									{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
@@ -437,15 +435,15 @@
 						<div class="ui attached segment comment-body">
 							<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}>
 								{{if .RenderedContent}}
-									{{.RenderedContent|Str2html}}
+									{{.RenderedContent}}
 								{{else}}
 									<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 								{{end}}
 							</div>
-							<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
-							<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
+							<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
+							<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
 							{{if .Attachments}}
-								{{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}}
+								{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
 							{{end}}
 						</div>
 						{{$reactions := .Reactions.GroupByType}}
@@ -456,7 +454,7 @@
 				</div>
 				{{end}}
 
-				{{if .Review.CodeComments}}
+				{{if and .Review .Review.CodeComments}}
 				<div class="timeline-item event">
 					{{range $filename, $lines := .Review.CodeComments}}
 						{{range $line, $comms := $lines}}
@@ -473,12 +471,12 @@
 				{{if .Content}}
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Poster}}
-						{{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr}}
 					</span>
 				{{else}}
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Poster}}
-						{{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr}}
 					</span>
 				{{end}}
 			</div>
@@ -488,16 +486,18 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 25}}
 			<div class="timeline-item event">
 				<span class="badge">{{svg "octicon-git-branch"}}</span>
-				{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{if not .OriginalAuthor}}
+					{{template "shared/user/avatarlink" dict "user" .Poster}}
+				{{end}}
 				<span class="text grey muted-links">
-					<a{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>{{.Poster.Name}}</a>
-					{{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}}
+					{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
+					{{ctx.Locale.Tr "repo.pulls.change_target_branch_at" .OldRef .NewRef $createdStr}}
 				</span>
 			</div>
 		{{else if eq .Type 26}}
@@ -507,9 +507,9 @@
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
 
-					{{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr | Safe}}
+					{{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr}}
 				</span>
-				<div class="detail">
+				<div class="detail flex-text-block">
 					{{svg "octicon-clock"}}
 					{{if .RenderedContent}}
 						{{/* compatibility with time comments made before v1.21 */}}
@@ -528,12 +528,12 @@
 					{{if (gt .AssigneeID 0)}}
 						{{if .RemovedAssignee}}
 							{{if eq .PosterID .AssigneeID}}
-								{{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr | Safe}}
+								{{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr}}
 							{{else}}
-								{{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}}
+								{{ctx.Locale.Tr "repo.issues.review.remove_review_request" .Assignee.GetDisplayName $createdStr}}
 							{{end}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.review.add_review_request" .Assignee.GetDisplayName $createdStr}}
 						{{end}}
 					{{else}}
 						<!-- If the assigned team is deleted, just displaying "Ghost Team" in the comment -->
@@ -542,9 +542,9 @@
 							{{$teamName = .AssigneeTeam.Name}}
 						{{end}}
 						{{if .RemovedAssignee}}
-							{{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.review.remove_review_request" $teamName $createdStr}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr | Safe}}
+							{{ctx.Locale.Tr "repo.issues.review.add_review_request" $teamName $createdStr}}
 						{{end}}
 					{{end}}
 				</span>
@@ -559,13 +559,13 @@
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
 					{{if .IsForcePush}}
-						{{ctx.Locale.Tr "repo.issues.force_push_codes" ($.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) (($.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) (($.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.force_push_codes" $.Issue.PullRequest.HeadBranch (ShortSha .OldCommit) ($.Issue.Repo.CommitLink .OldCommit) (ShortSha .NewCommit) ($.Issue.Repo.CommitLink .NewCommit) $createdStr}}
 					{{else}}
-						{{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr | Safe}}
+						{{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr}}
 					{{end}}
 				</span>
 				{{if and .IsForcePush $.Issue.PullRequest.BaseRepo.Name}}
-				<span class="gt-float-right comparebox">
+				<span class="tw-float-right comparebox">
 					<a href="{{$.Issue.PullRequest.BaseRepo.Link}}/compare/{{PathEscape .OldCommit}}..{{PathEscape .NewCommit}}" rel="nofollow" class="ui compare label">{{ctx.Locale.Tr "repo.issues.force_push_compare"}}</a>
 				</span>
 				{{end}}
@@ -583,19 +583,19 @@
 					{{$oldProjectDisplayHtml := "Unknown Project"}}
 					{{if .OldProject}}
 						{{$trKey := printf "projects.type-%d.display_name" .OldProject.Type}}
-						{{$oldProjectDisplayHtml = printf `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey | Escape) (.OldProject.Title | Escape)}}
+						{{$oldProjectDisplayHtml = HTMLFormat `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey) .OldProject.Title}}
 					{{end}}
 					{{$newProjectDisplayHtml := "Unknown Project"}}
 					{{if .Project}}
 						{{$trKey := printf "projects.type-%d.display_name" .Project.Type}}
-						{{$newProjectDisplayHtml = printf `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}}
+						{{$newProjectDisplayHtml = HTMLFormat `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey) .Project.Title}}
 					{{end}}
 					{{if and (gt .OldProjectID 0) (gt .ProjectID 0)}}
-						{{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr}}
 					{{else if gt .OldProjectID 0}}
-						{{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr}}
 					{{else if gt .ProjectID 0}}
-						{{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr}}
 					{{end}}
 				</span>
 			</div>
@@ -610,18 +610,20 @@
 					<span class="text grey muted-links">
 						{{template "shared/user/authorlink" .Poster}}
 						{{$reviewerName := ""}}
-						{{if eq .Review.OriginalAuthor ""}}
-							{{$reviewerName = .Review.Reviewer.Name}}
-						{{else}}
-							{{$reviewerName = .Review.OriginalAuthor}}
+						{{if .Review}}
+							{{if eq .Review.OriginalAuthor ""}}
+								{{$reviewerName = .Review.Reviewer.Name}}
+							{{else}}
+								{{$reviewerName = .Review.OriginalAuthor}}
+							{{end}}
 						{{end}}
-						{{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr}}
 					</span>
 				</div>
 				{{if .Content}}
 					<div class="timeline-item comment">
 						<div class="content">
-							<div class="ui top attached header comment-header-left gt-df gt-ac arrow-top">
+							<div class="ui top attached header comment-header-left tw-flex tw-items-center arrow-top">
 								{{if gt .Poster.ID 0}}
 									<a class="inline-timeline-avatar" href="{{.Poster.HomeLink}}">
 										{{ctx.AvatarUtils.Avatar .Poster 24}}
@@ -634,7 +636,7 @@
 							<div class="ui attached segment">
 								<div class="render-content markup">
 									{{if .RenderedContent}}
-										{{.RenderedContent|Str2html}}
+										{{.RenderedContent}}
 									{{else}}
 										<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
 									{{end}}
@@ -651,11 +653,11 @@
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
 					{{if and .OldRef .NewRef}}
-						{{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.change_ref_at" .OldRef .NewRef $createdStr}}
 					{{else if .OldRef}}
-						{{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.remove_ref_at" .OldRef $createdStr}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr | Safe}}
+						{{ctx.Locale.Tr "repo.issues.add_ref_at" .NewRef $createdStr}}
 					{{end}}
 				</span>
 			</div>
@@ -663,9 +665,9 @@
 			<div class="timeline-item event" id="{{.HashTag}}">
 				<span class="badge">{{svg "octicon-git-merge" 16}}</span>
 				<span class="text grey muted-links">
-					{{template "shared/user/authorlink" .Poster}}
-					{{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}}
-					{{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}}
+					{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
+					{{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr}}
+					{{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr}}{{end}}
 				</span>
 			</div>
 		{{else if or (eq .Type 36) (eq .Type 37)}}
@@ -674,8 +676,8 @@
 				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr | Safe}}
-					{{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr | Safe}}{{end}}
+					{{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr}}
+					{{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}}
 				</span>
 			</div>
 		{{end}}
diff --git a/templates/repo/issue/view_content/comments_authorlink.tmpl b/templates/repo/issue/view_content/comments_authorlink.tmpl
new file mode 100644
index 0000000000..f652a0bec3
--- /dev/null
+++ b/templates/repo/issue/view_content/comments_authorlink.tmpl
@@ -0,0 +1,11 @@
+{{if .comment.OriginalAuthor}}
+	<span class="text black">
+		{{svg (MigrationIcon .ctxData.Repository.GetOriginalURLHostname)}}
+		{{.comment.OriginalAuthor}}
+	</span>
+	{{if .ctxData.Repository.OriginalURL}}
+		<span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" .ctxData.Repository.OriginalURL .ctxData.Repository.GetOriginalURLHostname}})</span>
+	{{end}}
+{{else}}
+	{{template "shared/user/authorlink" .comment.Poster}}
+{{end}}
diff --git a/templates/repo/issue/view_content/comments_delete_time.tmpl b/templates/repo/issue/view_content/comments_delete_time.tmpl
index 7c01bb4228..2377e7c4f0 100644
--- a/templates/repo/issue/view_content/comments_delete_time.tmpl
+++ b/templates/repo/issue/view_content/comments_delete_time.tmpl
@@ -1,7 +1,7 @@
-{{if .comment.Time}} {{/* compatibility with time comments made before v1.14 */}}
+{{if and .comment.Time (.ctxData.Repository.IsTimetrackerEnabled ctx)}} {{/* compatibility with time comments made before v1.14 */}}
 	{{if (not .comment.Time.Deleted)}}
 		{{if (or .ctxData.IsAdmin (and .ctxData.IsSigned (eq .ctxData.SignedUserID .comment.PosterID)))}}
-			<span class="gt-float-right">
+			<span class="tw-float-right">
 				<div class="ui mini modal issue-delete-time-modal" data-id="{{.comment.Time.ID}}">
 					<form method="post" class="delete-time-form" action="{{.ctxData.RepoLink}}/issues/{{.ctxData.Issue.Index}}/times/{{.comment.TimeID}}/delete">
 						{{.ctxData.CsrfTokenHtml}}
diff --git a/templates/repo/issue/view_content/context_menu.tmpl b/templates/repo/issue/view_content/context_menu.tmpl
index 4afd73c371..17556d4e48 100644
--- a/templates/repo/issue/view_content/context_menu.tmpl
+++ b/templates/repo/issue/view_content/context_menu.tmpl
@@ -10,16 +10,33 @@
 			{{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
 		{{end}}
 		<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div>
-		{{if and .ctxData.IsSigned (not .ctxData.Repository.IsArchived)}}
-			<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
-			{{if not .ctxData.UnitIssuesGlobalDisabled}}
-				<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
+		{{if .ctxData.IsSigned}}
+			{{$needDivider := false}}
+			{{if not .ctxData.Repository.IsArchived}}
+				{{$needDivider = true}}
+				<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div>
+				{{if not .ctxData.UnitIssuesGlobalDisabled}}
+					<div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div>
+				{{end}}
+				{{if or .ctxData.Permission.IsAdmin .IsCommentPoster .ctxData.HasIssuesOrPullsWritePermission}}
+					<div class="divider"></div>
+					<div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
+					{{if .delete}}
+						<div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctxData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div>
+					{{end}}
+				{{end}}
 			{{end}}
-			{{if or .ctxData.Permission.IsAdmin .IsCommentPoster .ctxData.HasIssuesOrPullsWritePermission}}
-				<div class="divider"></div>
-				<div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
-				{{if .delete}}
-					<div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctxData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div>
+			{{$canUserBlock := call .ctxData.CanBlockUser .ctxData.SignedUser .item.Poster}}
+			{{$canOrgBlock := and .ctxData.Repository.Owner.IsOrganization (call .ctxData.CanBlockUser .ctxData.Repository.Owner .item.Poster)}}
+			{{if or $canOrgBlock $canUserBlock}}
+				{{if $needDivider}}
+					<div class="divider"></div>
+				{{end}}
+				{{if $canUserBlock}}
+				<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</div>
+				{{end}}
+				{{if $canOrgBlock}}
+				<div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{.ctxData.Repository.Owner.OrganisationLink}}/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.org"}}</div>
 				{{end}}
 			{{end}}
 		{{end}}
diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index c9e5ee6275..79e7cb498b 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -1,133 +1,143 @@
-{{$invalid := (index .comments 0).Invalidated}}
-{{$resolved := (index .comments 0).IsResolved}}
-{{$resolveDoer := (index .comments 0).ResolveDoer}}
-{{$isNotPending := (not (eq (index .comments 0).Review.Type 0))}}
-<div class="ui segments conversation-holder">
-	<div class="ui segment collapsible-comment-box gt-py-3 gt-df gt-ac gt-sb">
-		<div class="gt-df gt-ac">
-			<a href="{{(index .comments 0).CodeCommentLink ctx}}" class="file-comment gt-ml-3 gt-word-break">{{(index .comments 0).TreePath}}</a>
-			{{if $invalid}}
-				<span class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
-					{{ctx.Locale.Tr "repo.issues.review.outdated"}}
-				</span>
-			{{end}}
-		</div>
-		<div>
-			{{if or $invalid $resolved}}
-				<button id="show-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="{{if not $resolved}}gt-hidden {{end}}ui compact labeled button show-outdated gt-df gt-ac">
-					{{svg "octicon-unfold" 16 "gt-mr-3"}}
-					{{if $resolved}}
-						{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
-					{{else}}
-						{{ctx.Locale.Tr "repo.issues.review.show_outdated"}}
-					{{end}}
-				</button>
-				<button id="hide-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="{{if $resolved}}gt-hidden {{end}}ui compact labeled button hide-outdated gt-df gt-ac">
-					{{svg "octicon-fold" 16 "gt-mr-3"}}
-					{{if $resolved}}
-						{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
-					{{else}}
-						{{ctx.Locale.Tr "repo.issues.review.hide_outdated"}}
-					{{end}}
-				</button>
-			{{end}}
-		</div>
-	</div>
-	{{$diff := (CommentMustAsDiff ctx (index .comments 0))}}
-	{{if $diff}}
-		{{$file := (index $diff.Files 0)}}
-		<div id="code-preview-{{(index .comments 0).ID}}" class="ui table segment{{if $resolved}} gt-hidden{{end}}">
-			<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}}">
-				<div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped">
-					<table>
-						<tbody>
-							{{template "repo/diff/section_unified" dict "file" $file "root" $}}
-						</tbody>
-					</table>
-				</div>
-			</div>
-		</div>
-	{{end}}
-	<div id="code-comments-{{(index .comments 0).ID}}" class="comment-code-cloud ui segment{{if $resolved}} gt-hidden{{end}}">
-		<div class="ui comments gt-mb-0">
-			{{range .comments}}
-				{{$createdSubStr:= TimeSinceUnix .CreatedUnix ctx.Locale}}
-				<div class="comment code-comment gt-pb-4" id="{{.HashTag}}">
-					<div class="content">
-						<div class="header comment-header">
-							<div class="comment-header-left gt-df gt-ac">
-								{{if not .OriginalAuthor}}
-									<a class="avatar">
-										{{ctx.AvatarUtils.Avatar .Poster 20}}
-									</a>
-								{{end}}
-								<span class="text grey muted-links">
-									{{if .OriginalAuthor}}
-										<span class="text black gt-font-semibold">
-											{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
-											{{.OriginalAuthor}}
-										</span>
-										<span class="text grey muted-links"> {{if $.Repository.OriginalURL}}</span>
-										<span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}</span>
-									{{else}}
-										{{template "shared/user/authorlink" .Poster}}
-									{{end}}
-									{{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdSubStr | Safe}}
-								</span>
-							</div>
-							<div class="comment-header-right actions gt-df gt-ac">
-								{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
-								{{if not $.Repository.IsArchived}}
-									{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
-									{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
-								{{end}}
-							</div>
-						</div>
-						<div class="text comment-content">
-							<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}>
-							{{if .RenderedContent}}
-								{{.RenderedContent|Str2html}}
-							{{else}}
-								<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
-							{{end}}
-							</div>
-							<div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div>
-							<div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
-						</div>
-						{{$reactions := .Reactions.GroupByType}}
-						{{if $reactions}}
-							{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
-						{{end}}
-					</div>
-				</div>
-			{{end}}
-		</div>
-		<div class="code-comment-buttons gt-df gt-ac gt-fw gt-mt-3 gt-mb-2 gt-mx-3">
-			<div class="gt-f1">
-				{{if $resolved}}
-					<div class="ui grey text">
-						{{svg "octicon-check" 16 "gt-mr-2"}}
-						<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
-					</div>
+{{if len .comments}}
+	{{$comment := index .comments 0}}
+	{{$invalid := $comment.Invalidated}}
+	{{$resolved := $comment.IsResolved}}
+	{{$resolveDoer := $comment.ResolveDoer}}
+	{{$hasReview := and $comment.Review}}
+	{{$isReviewPending := and $hasReview (eq $comment.Review.Type 0)}}
+	<div class="ui segments conversation-holder">
+		<div class="ui segment collapsible-comment-box tw-py-2 tw-flex tw-items-center tw-justify-between">
+			<div class="tw-flex tw-items-center">
+				<a href="{{$comment.CodeCommentLink ctx}}" class="file-comment tw-ml-2 gt-word-break">{{$comment.TreePath}}</a>
+				{{if $invalid}}
+					<span class="ui label basic small tw-ml-2" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}">
+						{{ctx.Locale.Tr "repo.issues.review.outdated"}}
+					</span>
 				{{end}}
 			</div>
-			<div class="code-comment-buttons-buttons">
-				{{if and $.CanMarkConversation $isNotPending}}
-					<button class="ui tiny basic button resolve-conversation" data-origin="timeline" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{(index .comments 0).ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation">
+			<div>
+				{{if or $invalid $resolved}}
+					<button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}tw-hidden {{end}}ui compact labeled button show-outdated tw-flex tw-items-center">
+						{{svg "octicon-unfold" 16 "tw-mr-2"}}
 						{{if $resolved}}
-							{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
+							{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
 						{{else}}
-							{{ctx.Locale.Tr "repo.issues.review.resolve_conversation"}}
+							{{ctx.Locale.Tr "repo.issues.review.show_outdated"}}
 						{{end}}
 					</button>
-				{{end}}
-				{{if and $.SignedUserID (not $.Repository.IsArchived)}}
-					<button class="comment-form-reply ui primary tiny labeled icon button gt-ml-2 gt-mr-0">
-						{{svg "octicon-reply" 16 "reply icon gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
+					<button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}tw-hidden {{end}}ui compact labeled button hide-outdated tw-flex tw-items-center">
+						{{svg "octicon-fold" 16 "tw-mr-2"}}
+						{{if $resolved}}
+							{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
+						{{else}}
+							{{ctx.Locale.Tr "repo.issues.review.hide_outdated"}}
+						{{end}}
 					</button>
 				{{end}}
 			</div>
 		</div>
-		{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" (index .comments 0).ReviewID "root" $ "comment" (index .comments 0)}}
+		{{$diff := (CommentMustAsDiff ctx $comment)}}
+		{{if $diff}}
+			{{$file := (index $diff.Files 0)}}
+			<div id="code-preview-{{$comment.ID}}" class="ui table segment{{if $resolved}} tw-hidden{{end}}">
+				<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}}">
+					<div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped">
+						<table>
+							<tbody>
+								{{template "repo/diff/section_unified" dict "file" $file "root" $}}
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+		{{end}}
+		<div id="code-comments-{{$comment.ID}}" class="comment-code-cloud ui segment{{if $resolved}} tw-hidden{{end}}">
+			<div class="ui comments tw-mb-0">
+				{{range .comments}}
+					{{$createdSubStr:= TimeSinceUnix .CreatedUnix ctx.Locale}}
+					<div class="comment code-comment tw-pb-4" id="{{.HashTag}}">
+						<div class="content">
+							<div class="header comment-header">
+								<div class="comment-header-left tw-flex tw-items-center">
+									{{if not .OriginalAuthor}}
+										<a class="avatar">
+											{{ctx.AvatarUtils.Avatar .Poster 20}}
+										</a>
+									{{end}}
+									<span class="text grey muted-links">
+										{{if .OriginalAuthor}}
+											<span class="text black">
+												{{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}}
+												{{.OriginalAuthor}}
+											</span>
+											{{if $.Repository.OriginalURL}}
+											<span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}})</span>
+											{{end}}
+										{{else}}
+											{{template "shared/user/authorlink" .Poster}}
+										{{end}}
+										{{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdSubStr}}
+									</span>
+								</div>
+								<div class="comment-header-right actions tw-flex tw-items-center">
+									{{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}}
+									{{if not $.Repository.IsArchived}}
+										{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
+										{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
+									{{end}}
+								</div>
+							</div>
+							<div class="text comment-content">
+								<div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}>
+								{{if .RenderedContent}}
+									{{.RenderedContent}}
+								{{else}}
+									<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
+								{{end}}
+								</div>
+								<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
+								<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
+								{{if .Attachments}}
+									{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
+								{{end}}
+							</div>
+							{{$reactions := .Reactions.GroupByType}}
+							{{if $reactions}}
+								{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
+							{{end}}
+						</div>
+					</div>
+				{{end}}
+			</div>
+			<div class="code-comment-buttons tw-flex tw-items-center tw-flex-wrap tw-mt-2 tw-mb-1 tw-mx-2">
+				<div class="tw-flex-1">
+					{{if $resolved}}
+						<div class="ui grey text">
+							{{svg "octicon-check" 16 "tw-mr-1"}}
+							<b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}}
+						</div>
+					{{end}}
+				</div>
+				<div class="code-comment-buttons-buttons">
+					{{if and $.CanMarkConversation $hasReview (not $isReviewPending)}}
+						<button class="ui tiny basic button resolve-conversation" data-origin="timeline" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{$comment.ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation">
+							{{if $resolved}}
+								{{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}}
+							{{else}}
+								{{ctx.Locale.Tr "repo.issues.review.resolve_conversation"}}
+							{{end}}
+						</button>
+					{{end}}
+					{{if and $.SignedUserID (not $.Repository.IsArchived)}}
+						<button class="comment-form-reply ui primary tiny labeled icon button tw-ml-1 tw-mr-0">
+							{{svg "octicon-reply" 16 "reply icon tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
+						</button>
+					{{end}}
+				</div>
+			</div>
+			{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" $comment.ReviewID "root" $ "comment" $comment}}
+		</div>
 	</div>
-</div>
+{{else}}
+	{{template "repo/diff/conversation_outdated"}}
+{{end}}
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index 2b5776ea03..25e9bbcfc2 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -24,6 +24,7 @@
 		{{template "repo/pulls/status" (dict
 			"CommitStatus" .LatestCommitStatus
 			"CommitStatuses" .LatestCommitStatuses
+			"MissingRequiredChecks" .MissingRequiredChecks
 			"ShowHideChecks" true
 			"is_context_required" .is_context_required
 		)}}
@@ -32,13 +33,13 @@
 		<div class="ui attached merge-section segment {{if not $.LatestCommitStatus}}no-header{{end}} flex-items-block">
 			{{if .Issue.PullRequest.HasMerged}}
 				{{if .IsPullBranchDeletable}}
-					<div class="item item-section text gt-f1">
+					<div class="item item-section text tw-flex-1">
 						<div class="item-section-left">
-							<h3 class="gt-mb-3">
+							<h3 class="tw-mb-2">
 								{{ctx.Locale.Tr "repo.pulls.merged_success"}}
 							</h3>
 							<div class="merge-section-info">
-								{{ctx.Locale.Tr "repo.pulls.merged_info_text" (printf "<code>%s</code>" (.HeadTarget | Escape)) | Str2html}}
+								{{ctx.Locale.Tr "repo.pulls.merged_info_text" (HTMLFormat "<code>%s</code>" .HeadTarget)}}
 							</div>
 						</div>
 						<div class="item-section-right">
@@ -47,9 +48,9 @@
 					</div>
 				{{end}}
 			{{else if .Issue.IsClosed}}
-				<div class="item item-section text gt-f1">
+				<div class="item item-section text tw-flex-1">
 					<div class="item-section-left">
-						<h3 class="gt-mb-3">{{ctx.Locale.Tr "repo.pulls.closed"}}</h3>
+						<h3 class="tw-mb-2">{{ctx.Locale.Tr "repo.pulls.closed"}}</h3>
 						<div class="merge-section-info">
 							{{if .IsPullRequestBroken}}
 								{{ctx.Locale.Tr "repo.pulls.cant_reopen_deleted_branch"}}
@@ -80,14 +81,14 @@
 					{{ctx.Locale.Tr "repo.pulls.data_broken"}}
 				</div>
 			{{else if .IsPullWorkInProgress}}
-				<div class="item toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{(.WorkInProgressPrefix|Escape)}}" data-update-url="{{.Issue.Link}}/title">
-					<div class="item-section-left flex-text-inline gt-f1">
+				<div class="item toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{.WorkInProgressPrefix}}" data-update-url="{{.Issue.Link}}/title">
+					<div class="item-section-left flex-text-inline tw-flex-1">
 						{{svg "octicon-x"}}
 						{{ctx.Locale.Tr "repo.pulls.cannot_merge_work_in_progress"}}
 					</div>
 					{{if or .HasIssuesOrPullsWritePermission .IsIssuePoster}}
 						<button class="ui compact button">
-							{{ctx.Locale.Tr "repo.pulls.remove_prefix" (.WorkInProgressPrefix|Escape) | Safe}}
+							{{ctx.Locale.Tr "repo.pulls.remove_prefix" .WorkInProgressPrefix}}
 						</button>
 					{{end}}
 				</div>
@@ -126,7 +127,7 @@
 				{{else if .IsBlockedByChangedProtectedFiles}}
 					<div class="item">
 						{{svg "octicon-x"}}
-						{{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n" | Safe}}
+						{{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n"}}
 					</div>
 					<ul>
 						{{range .ChangedProtectedFiles}}
@@ -197,7 +198,7 @@
 				{{if .AllowMerge}} {{/* user is allowed to merge */}}
 					{{$prUnit := .Repository.MustGetUnit $.Context $.UnitTypePullRequests}}
 					{{$approvers := (.Issue.PullRequest.GetApprovers ctx)}}
-					{{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}}
+					{{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash $prUnit.PullRequestsConfig.AllowFastForwardOnly}}
 						{{$hasPendingPullRequestMergeTip := ""}}
 						{{if .HasPendingPullRequestMerge}}
 							{{$createdPRMergeStr := TimeSinceUnix .PendingPullRequestMerge.CreatedUnix ctx.Locale}}
@@ -268,6 +269,13 @@
 									'mergeMessageFieldText': {{.GetCommitMessages}} + defaultSquashMergeMessage,
 									'hideAutoMerge': generalHideAutoMerge,
 								},
+								{
+									'name': 'fast-forward-only',
+									'allowed': {{and $prUnit.PullRequestsConfig.AllowFastForwardOnly (eq .Issue.PullRequest.CommitsBehind 0)}},
+									'textDoMerge': {{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}},
+									'hideMergeMessageTexts': true,
+									'hideAutoMerge': generalHideAutoMerge,
+								},
 								{
 									'name': 'manually-merged',
 									'allowed': {{$prUnit.PullRequestsConfig.AllowManualMerge}},
@@ -326,7 +334,7 @@
 				{{else if .IsBlockedByChangedProtectedFiles}}
 					<div class="item text red">
 						{{svg "octicon-x"}}
-						{{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n" | Safe}}
+						{{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n"}}
 					</div>
 					<ul>
 						{{range .ChangedProtectedFiles}}
@@ -366,17 +374,15 @@
 			*/}}
 			{{if and $.StillCanManualMerge (not $showGeneralMergeForm)}}
 				<div class="divider"></div>
-				<div class="ui form">
-					<form action="{{.Link}}/merge" method="post">
-						{{.CsrfTokenHtml}}
-						<div class="field">
-							<input type="text" name="merge_commit_id" placeholder="{{ctx.Locale.Tr "repo.pulls.merge_commit_id"}}">
-						</div>
-						<button class="ui red button" type="submit" name="do" value="manually-merged">
-							{{ctx.Locale.Tr "repo.pulls.merge_manually"}}
-						</button>
-					</form>
-				</div>
+				<form class="ui form form-fetch-action" action="{{.Link}}/merge" method="post">{{/* another similar form is in PullRequestMergeForm.vue*/}}
+					{{.CsrfTokenHtml}}
+					<div class="field">
+						<input type="text" name="merge_commit_id" placeholder="{{ctx.Locale.Tr "repo.pulls.merge_commit_id"}}">
+					</div>
+					<button class="ui red button" type="submit" name="do" value="manually-merged">
+						{{ctx.Locale.Tr "repo.pulls.merge_manually"}}
+					</button>
+				</form>
 			{{end}}
 
 			{{if and .Issue.PullRequest.HeadRepo (not .Issue.PullRequest.HasMerged) (not .Issue.IsClosed)}}
diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
index 3dab44710e..d585d36574 100644
--- a/templates/repo/issue/view_content/pull_merge_instruction.tmpl
+++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
@@ -1,6 +1,6 @@
 <div class="divider"></div>
-<div class="instruct-toggle"> {{ctx.Locale.Tr "repo.pulls.cmd_instruction_hint" | Safe}} </div>
-<div class="instruct-content gt-mt-3 gt-hidden">
+<div class="instruct-toggle"> {{ctx.Locale.Tr "repo.pulls.cmd_instruction_hint"}} </div>
+<div class="instruct-content tw-mt-2 tw-hidden">
 	<div><h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_title"}}</h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_desc"}}</div>
 	{{$localBranch := .PullRequest.HeadBranch}}
 	{{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}
@@ -8,7 +8,7 @@
 	{{end}}
 	<div class="ui secondary segment">
 		{{if eq .PullRequest.Flow 0}}
-		<div>git fetch -u {{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}<gitea-origin-url data-url="{{.PullRequest.HeadRepo.Link}}"></gitea-origin-url>{{else}}origin{{end}} {{.PullRequest.HeadBranch}}:{{$localBranch}}</div>
+		<div>git fetch -u {{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}<origin-url data-url="{{.PullRequest.HeadRepo.Link}}"></origin-url>{{else}}origin{{end}} {{.PullRequest.HeadBranch}}:{{$localBranch}}</div>
 		<div>git checkout {{$localBranch}}</div>
 		{{else}}
 		<div>git fetch -u origin {{.GetGitRefName}}:{{$localBranch}}</div>
@@ -21,21 +21,25 @@
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge --no-ff {{$localBranch}}</div>
 		</div>
-		<div class="gt-hidden" data-pull-merge-style="rebase">
+		<div class="tw-hidden" data-pull-merge-style="rebase">
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge --ff-only {{$localBranch}}</div>
 		</div>
-		<div class="gt-hidden" data-pull-merge-style="rebase-merge">
+		<div class="tw-hidden" data-pull-merge-style="rebase-merge">
 			<div>git checkout {{$localBranch}}</div>
 			<div>git rebase {{.PullRequest.BaseBranch}}</div>
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge --no-ff {{$localBranch}}</div>
 		</div>
-		<div class="gt-hidden" data-pull-merge-style="squash">
+		<div class="tw-hidden" data-pull-merge-style="squash">
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge --squash {{$localBranch}}</div>
 		</div>
-		<div class="gt-hidden" data-pull-merge-style="manually-merged">
+		<div class="tw-hidden" data-pull-merge-style="fast-forward-only">
+			<div>git checkout {{.PullRequest.BaseBranch}}</div>
+			<div>git merge --ff-only {{$localBranch}}</div>
+		</div>
+		<div class="tw-hidden" data-pull-merge-style="manually-merged">
 			<div>git checkout {{.PullRequest.BaseBranch}}</div>
 			<div>git merge {{$localBranch}}</div>
 		</div>
diff --git a/templates/repo/issue/view_content/reference_issue_dialog.tmpl b/templates/repo/issue/view_content/reference_issue_dialog.tmpl
index b771e08909..5f338f6768 100644
--- a/templates/repo/issue/view_content/reference_issue_dialog.tmpl
+++ b/templates/repo/issue/view_content/reference_issue_dialog.tmpl
@@ -2,7 +2,7 @@
 	<div class="header">
 		{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}
 	</div>
-	<div class="content" style="text-align:left">
+	<div class="content tw-text-left">
 		<form class="ui form form-fetch-action" action="{{printf "%s/issues/new" .Repository.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="ui segment content">
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 6c13eef023..7040c2849a 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -2,11 +2,11 @@
 	{{template "repo/issue/branch_selector_field" .}}
 	{{if .Issue.IsPull}}
 		<input id="reviewer_id" name="reviewer_id" type="hidden" value="{{.reviewer_id}}">
-		<div class="ui {{if or (not .Reviewers) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown">
-			<a class="text gt-df gt-ac muted">
+		<div class="ui {{if or (and (not .Reviewers) (not .TeamReviewers)) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown">
+			<a class="text tw-flex tw-items-center muted">
 				<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong>
 				{{if and .CanChooseReviewer (not .Repository.IsArchived)}}
-					{{svg "octicon-gear" 16 "gt-ml-2"}}
+					{{svg "octicon-gear" 16 "tw-ml-1"}}
 				{{end}}
 			</a>
 			<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/request_review">
@@ -20,22 +20,24 @@
 					{{range .Reviewers}}
 						{{if .User}}
 							<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_{{.ItemID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
-								<span class="octicon-check {{if not .Checked}}gt-invisible{{end}}">{{svg "octicon-check"}}</span>
+								<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
 								<span class="text">
-									{{ctx.AvatarUtils.Avatar .User 28 "gt-mr-3"}}{{template "repo/search_name" .User}}
+									{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}{{template "repo/search_name" .User}}
 								</span>
 							</a>
 						{{end}}
 					{{end}}
 				{{end}}
 				{{if .TeamReviewers}}
-					<div class="divider"></div>
+					{{if .Reviewers}}
+						<div class="divider"></div>
+					{{end}}
 					{{range .TeamReviewers}}
 						{{if .Team}}
 							<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_team_{{.Team.ID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
-								<span class="octicon-check {{if not .Checked}}gt-invisible{{end}}">{{svg "octicon-check" 16}}</span>
+								<span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check" 16}}</span>
 								<span class="text">
-									{{svg "octicon-people" 16 "gt-ml-4 gt-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}
+									{{svg "octicon-people" 16 "tw-ml-4 tw-mr-1"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}
 								</span>
 							</a>
 						{{end}}
@@ -45,20 +47,20 @@
 		</div>
 
 		<div class="ui assignees list">
-			<span class="no-select item {{if or .OriginalReviews .PullReviewers}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span>
+			<span class="no-select item {{if or .OriginalReviews .PullReviewers}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span>
 			<div class="selected">
 				{{range .PullReviewers}}
-					<div class="item gt-df gt-ac gt-py-3">
-						<div class="gt-df gt-ac gt-f1">
+					<div class="item tw-flex tw-items-center tw-py-2">
+						<div class="tw-flex tw-items-center tw-flex-1">
 							{{if .User}}
-								<a class="muted sidebar-item-link" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20 "gt-mr-3"}}{{.User.GetDisplayName}}</a>
+								<a class="muted sidebar-item-link" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20 "tw-mr-2"}}{{.User.GetDisplayName}}</a>
 							{{else if .Team}}
-								<span class="text">{{svg "octicon-people" 20 "gt-mr-3"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span>
+								<span class="text">{{svg "octicon-people" 20 "tw-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span>
 							{{end}}
 						</div>
-						<div class="gt-df gt-ac gt-gap-3">
-							{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed))}}
-								<a href="#" class="ui muted icon gt-df gt-ac show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
+						<div class="tw-flex tw-items-center tw-gap-2">
+							{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged))}}
+								<a href="#" class="ui muted icon tw-flex tw-items-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
 									{{svg "octicon-x" 20}}
 								</a>
 								<div class="ui small modal" id="dismiss-review-modal-{{.Review.ID}}">
@@ -89,7 +91,7 @@
 									{{svg "octicon-hourglass" 16}}
 								</span>
 							{{end}}
-							{{if .CanChange}}
+							{{if and .CanChange (or .Checked (and (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged)))}}
 								<a href="#" class="ui muted icon re-request-review{{if .Checked}} checked{{end}}" data-tooltip-content="{{if .Checked}}{{ctx.Locale.Tr "repo.issues.remove_request_review"}}{{else}}{{ctx.Locale.Tr "repo.issues.re_request_review"}}{{end}}" data-issue-id="{{$.Issue.ID}}" data-id="{{.ItemID}}" data-update-url="{{$.RepoLink}}/issues/request_review">{{if .Checked}}{{svg "octicon-trash"}}{{else}}{{svg "octicon-sync"}}{{end}}</a>
 							{{end}}
 							{{svg (printf "octicon-%s" .Review.Type.Icon) 16 (printf "text %s" (.Review.HTMLTypeColorName))}}
@@ -97,14 +99,14 @@
 					</div>
 				{{end}}
 				{{range .OriginalReviews}}
-					<div class="item gt-df gt-ac gt-py-3">
-						<div class="gt-df gt-ac gt-f1">
-							<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" ($.Repository.GetOriginalURLHostname|Escape) | Safe}}">
-								{{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "gt-mr-3"}}
+					<div class="item tw-flex tw-items-center tw-py-2">
+						<div class="tw-flex tw-items-center tw-flex-1">
+							<a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $.Repository.GetOriginalURLHostname}}">
+								{{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "tw-mr-2"}}
 								{{.OriginalAuthor}}
 							</a>
 						</div>
-						<div class="gt-df gt-ac gt-gap-3">
+						<div class="tw-flex tw-items-center tw-gap-2">
 							{{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}}
 						</div>
 					</div>
@@ -112,9 +114,9 @@
 			</div>
 		</div>
 		{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .HasMerged) (not .Issue.IsClosed) (not .IsPullWorkInProgress)}}
-			<div class="toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{(index .PullRequestWorkInProgressPrefixes 0| Escape)}}" data-update-url="{{.Issue.Link}}/title">
+			<div class="toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{index .PullRequestWorkInProgressPrefixes 0}}" data-update-url="{{.Issue.Link}}/title">
 				<a class="muted">
-					{{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}
+					{{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0)}}
 				</a>
 			</div>
 		{{end}}
@@ -130,7 +132,7 @@
 		<a class="text muted flex-text-block">
 			<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong>
 			{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
-				{{svg "octicon-gear" 16 "gt-ml-2"}}
+				{{svg "octicon-gear" 16 "tw-ml-1"}}
 			{{end}}
 		</a>
 		<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/milestone">
@@ -138,11 +140,11 @@
 		</div>
 	</div>
 	<div class="ui select-milestone list">
-		<span class="no-select item {{if .Issue.Milestone}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
+		<span class="no-select item {{if .Issue.Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
 		<div class="selected">
 			{{if .Issue.Milestone}}
 				<a class="item muted sidebar-item-link" href="{{.RepoLink}}/milestone/{{.Issue.Milestone.ID}}">
-					{{svg "octicon-milestone" 18 "gt-mr-3"}}
+					{{svg "octicon-milestone" 18 "tw-mr-2"}}
 					{{.Issue.Milestone.Name}}
 				</a>
 			{{end}}
@@ -156,7 +158,7 @@
 			<a class="text muted flex-text-block">
 				<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong>
 				{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
-					{{svg "octicon-gear" 16 "gt-ml-2"}}
+					{{svg "octicon-gear" 16 "tw-ml-1"}}
 				{{end}}
 			</a>
 			<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/projects">
@@ -174,7 +176,7 @@
 					</div>
 					{{range .OpenProjects}}
 						<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
-							{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+							{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 						</a>
 					{{end}}
 				{{end}}
@@ -185,18 +187,18 @@
 					</div>
 					{{range .ClosedProjects}}
 						<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}">
-							{{svg .IconName 18 "gt-mr-3"}}{{.Title}}
+							{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
 						</a>
 					{{end}}
 				{{end}}
 			</div>
 		</div>
 		<div class="ui select-project list">
-			<span class="no-select item {{if .Issue.Project}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
+			<span class="no-select item {{if .Issue.Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
 			<div class="selected">
 				{{if .Issue.Project}}
 					<a class="item muted sidebar-item-link" href="{{.Issue.Project.Link ctx}}">
-						{{svg .Issue.Project.IconName 18 "gt-mr-3"}}{{.Issue.Project.Title}}
+						{{svg .Issue.Project.IconName 18 "tw-mr-2"}}{{.Issue.Project.Title}}
 					</a>
 				{{end}}
 			</div>
@@ -210,7 +212,7 @@
 		<a class="text muted flex-text-block">
 			<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong>
 			{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
-				{{svg "octicon-gear" 16 "gt-ml-2"}}
+				{{svg "octicon-gear" 16 "tw-ml-1"}}
 			{{end}}
 		</a>
 		<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee">
@@ -229,21 +231,21 @@
 							{{$checked = true}}
 						{{end}}
 					{{end}}
-					<span class="octicon-check {{if not $checked}}gt-invisible{{end}}">{{svg "octicon-check"}}</span>
+					<span class="octicon-check {{if not $checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
 					<span class="text">
-						{{ctx.AvatarUtils.Avatar . 20 "gt-mr-3"}}{{template "repo/search_name" .}}
+						{{ctx.AvatarUtils.Avatar . 20 "tw-mr-2"}}{{template "repo/search_name" .}}
 					</span>
 				</a>
 			{{end}}
 		</div>
 	</div>
 	<div class="ui assignees list">
-		<span class="no-select item {{if .Issue.Assignees}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span>
+		<span class="no-select item {{if .Issue.Assignees}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span>
 		<div class="selected">
 			{{range .Issue.Assignees}}
 				<div class="item">
 					<a class="muted sidebar-item-link" href="{{$.RepoLink}}/{{if $.Issue.IsPull}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
-						{{ctx.AvatarUtils.Avatar . 28 "gt-mr-3"}}
+						{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}
 						{{.GetDisplayName}}
 					</a>
 				</div>
@@ -255,10 +257,10 @@
 
 	{{if .Participants}}
 		<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.num_participants" .NumParticipants}}</strong></span>
-		<div class="ui list gt-df gt-fw">
+		<div class="ui list tw-flex tw-flex-wrap">
 			{{range .Participants}}
 				<a {{if gt .ID 0}}href="{{.HomeLink}}"{{end}} data-tooltip-content="{{.GetDisplayName}}">
-					{{ctx.AvatarUtils.Avatar . 28 "gt-my-1 gt-mr-2"}}
+					{{ctx.AvatarUtils.Avatar . 28 "tw-my-0.5 tw-mr-1"}}
 				</a>
 			{{end}}
 		</div>
@@ -269,7 +271,7 @@
 
 		<div class="ui watching">
 			<span class="text"><strong>{{ctx.Locale.Tr "notification.notifications"}}</strong></span>
-			<div class="gt-mt-3">
+			<div class="tw-mt-2">
 				{{template "repo/issue/view_content/watching" .}}
 			</div>
 		</div>
@@ -279,7 +281,7 @@
 			<div class="divider"></div>
 			<div class="ui timetrack">
 				<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong></span>
-				<div class="gt-mt-3">
+				<div class="tw-mt-2">
 					<form method="post" action="{{.Issue.Link}}/times/stopwatch/toggle" id="toggle_stopwatch_form">
 						{{$.CsrfTokenHtml}}
 					</form>
@@ -288,27 +290,27 @@
 					</form>
 					{{if $.IsStopwatchRunning}}
 						<button class="ui fluid button issue-stop-time">
-							{{svg "octicon-stopwatch" 16 "gt-mr-3"}}
+							{{svg "octicon-stopwatch" 16 "tw-mr-2"}}
 							{{ctx.Locale.Tr "repo.issues.stop_tracking"}}
 						</button>
-						<button class="ui fluid button issue-cancel-time gt-mt-3">
-							{{svg "octicon-trash" 16 "gt-mr-3"}}
+						<button class="ui fluid button issue-cancel-time tw-mt-2">
+							{{svg "octicon-trash" 16 "tw-mr-2"}}
 							{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}
 						</button>
 					{{else}}
 						{{if .HasUserStopwatch}}
 							<div class="ui warning message">
-								{{ctx.Locale.Tr "repo.issues.tracking_already_started" (.OtherStopwatchURL|Escape) | Safe}}
+								{{ctx.Locale.Tr "repo.issues.tracking_already_started" .OtherStopwatchURL}}
 							</div>
 						{{end}}
 						<button class="ui fluid button issue-start-time" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.start_tracking"}}'>
-							{{svg "octicon-stopwatch" 16 "gt-mr-3"}}
+							{{svg "octicon-stopwatch" 16 "tw-mr-2"}}
 							{{ctx.Locale.Tr "repo.issues.start_tracking_short"}}
 						</button>
 						<div class="ui mini modal issue-start-time-modal">
 							<div class="header">{{ctx.Locale.Tr "repo.issues.add_time"}}</div>
 							<div class="content">
-								<form method="post" id="add_time_manual_form" action="{{.Issue.Link}}/times/add" class="ui input fluid gt-gap-3">
+								<form method="post" id="add_time_manual_form" action="{{.Issue.Link}}/times/add" class="ui input fluid tw-gap-2">
 									{{$.CsrfTokenHtml}}
 									<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_hours"}}' type="number" name="hours">
 									<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_minutes"}}' type="number" name="minutes" class="ui compact">
@@ -319,8 +321,8 @@
 								<button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.add_time_cancel"}}</button>
 							</div>
 						</div>
-						<button class="ui fluid button issue-add-time gt-mt-3" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.add_time"}}'>
-							{{svg "octicon-plus" 16 "gt-mr-3"}}
+						<button class="ui fluid button issue-add-time tw-mt-2" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.add_time"}}'>
+							{{svg "octicon-plus" 16 "tw-mr-2"}}
 							{{ctx.Locale.Tr "repo.issues.add_time_short"}}
 						</button>
 					{{end}}
@@ -330,10 +332,10 @@
 		{{if .WorkingUsers}}
 			<div class="divider"></div>
 			<div class="ui comments">
-				<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time) | Safe}}</strong></span>
+				<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}}</strong></span>
 				<div>
 					{{range $user, $trackedtime := .WorkingUsers}}
-						<div class="comment gt-mt-3">
+						<div class="comment tw-mt-2">
 							<a class="avatar">
 								{{ctx.AvatarUtils.Avatar $user}}
 							</a>
@@ -353,20 +355,20 @@
 	<div class="divider"></div>
 	<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.due_date"}}</strong></span>
 	<div class="ui form" id="deadline-loader">
-		<div class="ui negative message gt-hidden" id="deadline-err-invalid-date">
+		<div class="ui negative message tw-hidden" id="deadline-err-invalid-date">
 			{{svg "octicon-x" 16 "close icon"}}
 			{{ctx.Locale.Tr "repo.issues.due_date_invalid"}}
 		</div>
 		{{if ne .Issue.DeadlineUnix 0}}
 			<p>
-				<div class="gt-df gt-sb gt-ac">
+				<div class="tw-flex tw-justify-between tw-items-center">
 					<div class="due-date {{if .Issue.IsOverdue}}text red{{end}}" {{if .Issue.IsOverdue}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_overdue"}}"{{end}}>
-						{{svg "octicon-calendar" 16 "gt-mr-3"}}
+						{{svg "octicon-calendar" 16 "tw-mr-2"}}
 						{{DateTime "long" .Issue.DeadlineUnix.FormatDate}}
 					</div>
 					<div>
 						{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
-							<a class="issue-due-edit muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_edit"}}">{{svg "octicon-pencil" 16 "gt-mr-2"}}</a>
+							<a class="issue-due-edit muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_edit"}}">{{svg "octicon-pencil" 16 "tw-mr-1"}}</a>
 							<a class="issue-due-remove muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_remove"}}">{{svg "octicon-trash"}}</a>
 						{{end}}
 					</div>
@@ -377,7 +379,7 @@
 		{{end}}
 
 		{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
-			<div {{if ne .Issue.DeadlineUnix 0}} class="gt-hidden"{{end}} id="deadlineForm">
+			<div {{if ne .Issue.DeadlineUnix 0}} class="tw-hidden"{{end}} id="deadlineForm">
 				<form class="ui fluid action input issue-due-form" action="{{AppSubUrl}}/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}/deadline" method="post" id="update-issue-deadline-form">
 					{{$.CsrfTokenHtml}}
 					<input required placeholder="{{ctx.Locale.Tr "repo.issues.due_date_form"}}" {{if gt .Issue.DeadlineUnix 0}}value="{{.Issue.DeadlineUnix.FormatDate}}"{{end}} type="date" name="deadlineDate" id="deadlineDate">
@@ -415,8 +417,8 @@
 				</span>
 				<div class="ui relaxed divided list">
 					{{range .BlockingDependencies}}
-						<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} gt-df gt-ac gt-sb">
-							<div class="item-left gt-df gt-jc gt-fc gt-f1 gt-ellipsis">
+						<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
+							<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 								<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
 									#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}
 								</a>
@@ -424,7 +426,7 @@
 									{{.Repository.OwnerName}}/{{.Repository.Name}}
 								</div>
 							</div>
-							<div class="item-right gt-df gt-ac gt-m-2">
+							<div class="item-right tw-flex tw-items-center tw-m-1">
 								{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
 									<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
 										{{svg "octicon-trash" 16}}
@@ -434,7 +436,7 @@
 						</div>
 					{{end}}
 					{{if .BlockingDependenciesNotPermitted}}
-						<div class="item gt-df gt-ac gt-sb gt-ellipsis">
+						<div class="item tw-flex tw-items-center tw-justify-between gt-ellipsis">
 							<span>{{ctx.Locale.TrN (len .BlockingDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockingDependenciesNotPermitted)}}</span>
 						</div>
 					{{end}}
@@ -447,8 +449,8 @@
 				</span>
 				<div class="ui relaxed divided list">
 					{{range .BlockedByDependencies}}
-						<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} gt-df gt-ac gt-sb">
-							<div class="item-left gt-df gt-jc gt-fc gt-f1 gt-ellipsis">
+						<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
+							<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 								<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
 									#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}
 								</a>
@@ -456,7 +458,7 @@
 									{{.Repository.OwnerName}}/{{.Repository.Name}}
 								</div>
 							</div>
-							<div class="item-right gt-df gt-ac gt-m-2">
+							<div class="item-right tw-flex tw-items-center tw-m-1">
 								{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
 									<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blockedBy" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
 										{{svg "octicon-trash" 16}}
@@ -467,8 +469,8 @@
 					{{end}}
 					{{if $.CanCreateIssueDependencies}}
 						{{range .BlockedByDependenciesNotPermitted}}
-							<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} gt-df gt-ac gt-sb">
-								<div class="item-left gt-df gt-jc gt-fc gt-f1 gt-ellipsis">
+							<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
+								<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 									<div class="gt-ellipsis">
 										<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span>
 										<span class="title" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
@@ -479,7 +481,7 @@
 										{{.Repository.OwnerName}}/{{.Repository.Name}}
 									</div>
 								</div>
-								<div class="item-right gt-df gt-ac gt-m-2">
+								<div class="item-right tw-flex tw-items-center tw-m-1">
 									{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
 										<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}">
 											{{svg "octicon-trash" 16}}
@@ -489,7 +491,7 @@
 							</div>
 						{{end}}
 					{{else if .BlockedByDependenciesNotPermitted}}
-						<div class="item gt-df gt-ac gt-sb gt-ellipsis">
+						<div class="item tw-flex tw-items-center tw-justify-between gt-ellipsis">
 							<span>{{ctx.Locale.TrN (len .BlockedByDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockedByDependenciesNotPermitted)}}</span>
 						</div>
 					{{end}}
@@ -546,9 +548,9 @@
 	<div class="divider"></div>
 	<div class="ui equal width compact grid">
 		{{$issueReferenceLink := printf "%s#%d" .Issue.Repo.FullName .Issue.Index}}
-		<div class="row gt-ac" data-tooltip-content="{{$issueReferenceLink}}">
+		<div class="row tw-items-center" data-tooltip-content="{{$issueReferenceLink}}">
 			<span class="text column truncate">{{ctx.Locale.Tr "repo.issues.reference_link" $issueReferenceLink}}</span>
-			<button class="ui two wide button column gt-p-3" data-clipboard-text="{{$issueReferenceLink}}">{{svg "octicon-copy" 14}}</button>
+			<button class="ui two wide button column tw-p-2" data-clipboard-text="{{$issueReferenceLink}}">{{svg "octicon-copy" 14}}</button>
 		</div>
 	</div>
 
@@ -556,21 +558,21 @@
 		<div class="divider"></div>
 
 		{{if or .PinEnabled .Issue.IsPinned}}
-			<form class="gt-mt-2 form-fetch-action single-button-form" method="post" {{if $.NewPinAllowed}}action="{{.Issue.Link}}/pin"{{else}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.max_pinned"}}"{{end}}>
+			<form class="tw-mt-1 form-fetch-action single-button-form" method="post" {{if $.NewPinAllowed}}action="{{.Issue.Link}}/pin"{{else}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.max_pinned"}}"{{end}}>
 				{{$.CsrfTokenHtml}}
 				<button class="fluid ui button {{if not $.NewPinAllowed}}disabled{{end}}">
 					{{if not .Issue.IsPinned}}
-						{{svg "octicon-pin" 16 "gt-mr-3"}}
+						{{svg "octicon-pin" 16 "tw-mr-2"}}
 						{{ctx.Locale.Tr "pin"}}
 					{{else}}
-						{{svg "octicon-pin-slash" 16 "gt-mr-3"}}
+						{{svg "octicon-pin-slash" 16 "tw-mr-2"}}
 						{{ctx.Locale.Tr "unpin"}}
 					{{end}}
 				</button>
 			</form>
 		{{end}}
 
-		<button class="gt-mt-2 fluid ui show-modal button {{if .Issue.IsLocked}} negative {{end}}" data-modal="#lock">
+		<button class="tw-mt-1 fluid ui show-modal button {{if .Issue.IsLocked}} negative {{end}}" data-modal="#lock">
 			{{if .Issue.IsLocked}}
 				{{svg "octicon-key"}}
 				{{ctx.Locale.Tr "repo.issues.unlock"}}
@@ -643,7 +645,7 @@
 				</form>
 			</div>
 		</div>
-		<button class="gt-mt-2 fluid ui show-modal button" data-modal="#sidebar-delete-issue">
+		<button class="tw-mt-1 fluid ui show-modal button" data-modal="#sidebar-delete-issue">
 			{{svg "octicon-trash"}}
 			{{ctx.Locale.Tr "repo.issues.delete"}}
 		</button>
@@ -675,7 +677,7 @@
 		{{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
 			<div class="divider"></div>
 			<div class="inline field">
-				<div class="ui checkbox" id="allow-edits-from-maintainers"
+				<div class="ui checkbox loading-icon-2px" id="allow-edits-from-maintainers"
 						data-url="{{.Issue.Link}}"
 						data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"
 						data-prompt-error="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_err"}}"
diff --git a/templates/repo/issue/view_content/update_branch_by_merge.tmpl b/templates/repo/issue/view_content/update_branch_by_merge.tmpl
index 4dbefefe00..adce052dee 100644
--- a/templates/repo/issue/view_content/update_branch_by_merge.tmpl
+++ b/templates/repo/issue/view_content/update_branch_by_merge.tmpl
@@ -7,7 +7,7 @@
 		</div>
 		<div class="item-section-right">
 			{{if and $.UpdateAllowed $.UpdateByRebaseAllowed}}
-				<div class="gt-dib">
+				<div class="tw-inline-block">
 					<div class="ui buttons update-button">
 						<button class="ui button" data-do="{{$.Link}}/update" data-redirect="{{$.Link}}">
 							<span class="button-text">
diff --git a/templates/repo/issue/view_content/watching.tmpl b/templates/repo/issue/view_content/watching.tmpl
index 0e8562fed2..05936d090b 100644
--- a/templates/repo/issue/view_content/watching.tmpl
+++ b/templates/repo/issue/view_content/watching.tmpl
@@ -2,10 +2,10 @@
 	<input type="hidden" name="watch" value="{{if $.IssueWatch.IsWatching}}0{{else}}1{{end}}">
 	<button class="fluid ui button">
 		{{if $.IssueWatch.IsWatching}}
-			{{svg "octicon-mute" 16 "gt-mr-3"}}
+			{{svg "octicon-mute" 16 "tw-mr-2"}}
 			{{ctx.Locale.Tr "repo.issues.unsubscribe"}}
 		{{else}}
-			{{svg "octicon-unmute" 16 "gt-mr-3"}}
+			{{svg "octicon-unmute" 16 "tw-mr-2"}}
 			{{ctx.Locale.Tr "repo.issues.subscribe"}}
 		{{end}}
 	</button>
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 7ec48c6734..b78ff55cda 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -1,5 +1,5 @@
 {{if .Flash}}
-	<div class="sixteen wide column gt-mb-3">
+	<div class="sixteen wide column tw-mb-2">
 		{{template "base/alert" .}}
 	</div>
 {{end}}
@@ -8,28 +8,28 @@
 		<h1 class="gt-word-break">
 			<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span>
 </span>
-			<div id="edit-title-input" class="ui input gt-f1 gt-hidden">
+			<div id="edit-title-input" class="ui input tw-flex-1 tw-hidden">
 				<input value="{{.Issue.Title}}" maxlength="255" autocomplete="off">
 			</div>
 		</h1>
 		<div class="issue-title-buttons">
 			{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
-				<button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} gt-mr-0{{end}}">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
+				<button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} tw-mr-0{{end}}">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
 			{{end}}
 			{{if not .Issue.IsPull}}
-				<a role="button" class="ui small primary button new-issue-button gt-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
+				<a role="button" class="ui small primary button new-issue-button tw-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
 			{{end}}
 		</div>
 		{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
 			<div class="edit-buttons">
-				<button id="cancel-edit-title" class="ui small basic button in-edit gt-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
-				<button id="save-edit-title" class="ui small primary button in-edit gt-hidden gt-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button>
+				<button id="cancel-edit-title" class="ui small basic button in-edit tw-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
+				<button id="save-edit-title" class="ui small primary button in-edit tw-hidden tw-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button>
 			</div>
 		{{end}}
 	</div>
 	<div class="issue-title-meta">
 		{{if .HasMerged}}
-			<div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "gt-mr-2"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div>
+			<div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div>
 		{{else if .Issue.IsClosed}}
 			<div class="ui red label issue-state-label">{{if .Issue.IsPull}}{{svg "octicon-git-pull-request"}}{{else}}{{svg "octicon-issue-closed"}}{{end}} {{ctx.Locale.Tr "repo.issues.closed_title"}}</div>
 		{{else if .Issue.IsPull}}
@@ -41,43 +41,43 @@
 		{{else}}
 			<div class="ui green label issue-state-label">{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues.open_title"}}</div>
 		{{end}}
-		<div class="gt-ml-3">
+		<div class="tw-ml-2">
 			{{if .Issue.IsPull}}
-				{{$headHref := .HeadTarget|Escape}}
+				{{$headHref := .HeadTarget}}
 				{{if .HeadBranchLink}}
-					{{$headHref = printf `<a href="%s">%s</a>` (.HeadBranchLink | Escape) $headHref}}
+					{{$headHref = HTMLFormat `<a href="%s">%s</a>` .HeadBranchLink $headHref}}
 				{{end}}
-				{{$headHref = printf `%s <button class="btn interact-fg" data-tooltip-content="%s" data-clipboard-text="%s">%s</button>` $headHref (ctx.Locale.Tr "copy_branch") (.HeadTarget | Escape) (svg "octicon-copy" 14)}}
-				{{$baseHref := .BaseTarget|Escape}}
+				{{$headHref = HTMLFormat `%s <button class="btn interact-fg" data-tooltip-content="%s" data-clipboard-text="%s">%s</button>` $headHref (ctx.Locale.Tr "copy_branch") .HeadTarget (svg "octicon-copy" 14)}}
+				{{$baseHref := .BaseTarget}}
 				{{if .BaseBranchLink}}
-					{{$baseHref = printf `<a href="%s">%s</a>` (.BaseBranchLink | Escape) $baseHref}}
+					{{$baseHref = HTMLFormat `<a href="%s">%s</a>` .BaseBranchLink $baseHref}}
 				{{end}}
 				{{if .Issue.PullRequest.HasMerged}}
 					{{$mergedStr:= TimeSinceUnix .Issue.PullRequest.MergedUnix ctx.Locale}}
 					{{if .Issue.OriginalAuthor}}
 						{{.Issue.OriginalAuthor}}
-						<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr | Safe}}</span>
+						<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}</span>
 					{{else}}
 						<a {{if gt .Issue.PullRequest.Merger.ID 0}}href="{{.Issue.PullRequest.Merger.HomeLink}}"{{end}}>{{.Issue.PullRequest.Merger.GetDisplayName}}</a>
-						<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr | Safe}}</span>
+						<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}</span>
 					{{end}}
 				{{else}}
 					{{if .Issue.OriginalAuthor}}
-						<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref | Safe}}</span>
+						<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span>
 					{{else}}
 						<span id="pull-desc" class="pull-desc">
 							<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a>
-							{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref | Safe}}
+							{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}
 						</span>
 					{{end}}
-					<span id="pull-desc-edit" class="gt-hidden flex-text-block">
+					<span id="pull-desc-edit" class="tw-hidden flex-text-block">
 						<div class="ui floating filter dropdown">
-							<div class="ui basic small button gt-mr-0">
+							<div class="ui basic small button tw-mr-0">
 								<span class="text">{{ctx.Locale.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span>
 							</div>
 						</div>
 						{{svg "octicon-arrow-right"}}
-						<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "repo.pulls.no_results"}}">
+						<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}">
 							<div class="ui basic small button">
 								<span class="text" id="pull-target-branch" data-basename="{{$.BaseName}}" data-branch="{{$.BaseBranch}}">{{ctx.Locale.Tr "repo.pulls.compare_base"}}: {{$.BaseName}}:{{$.BaseBranch}}</span>
 								{{svg "octicon-triangle-down" 14 "dropdown icon"}}
@@ -104,11 +104,11 @@
 				{{$createdStr:= TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
 				<span class="time-desc">
 					{{if .Issue.OriginalAuthor}}
-						{{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr (.Issue.OriginalAuthor|Escape) | Safe}}
+						{{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr .Issue.OriginalAuthor}}
 					{{else if gt .Issue.Poster.ID 0}}
-						{{ctx.Locale.Tr "repo.issues.opened_by" $createdStr (.Issue.Poster.HomeLink|Escape) (.Issue.Poster.GetDisplayName|Escape) | Safe}}
+						{{ctx.Locale.Tr "repo.issues.opened_by" $createdStr .Issue.Poster.HomeLink .Issue.Poster.GetDisplayName}}
 					{{else}}
-						{{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr (.Issue.Poster.GetDisplayName|Escape) | Safe}}
+						{{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr .Issue.Poster.GetDisplayName}}
 					{{end}}
 					·
 					{{ctx.Locale.TrN .Issue.NumComments "repo.issues.num_comments_1" "repo.issues.num_comments" .Issue.NumComments}}
diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl
index b2f0798917..8bacb427bf 100644
--- a/templates/repo/latest_commit.tmpl
+++ b/templates/repo/latest_commit.tmpl
@@ -2,15 +2,15 @@
 	<div class="ui active tiny slow centered inline">…</div>
 {{else}}
 	{{if .LatestCommitUser}}
-		{{ctx.AvatarUtils.Avatar .LatestCommitUser 24 "gt-mr-2"}}
-		{{if .LatestCommitUser.FullName}}
+		{{ctx.AvatarUtils.Avatar .LatestCommitUser 24 "tw-mr-1"}}
+		{{if and .LatestCommitUser.FullName DefaultShowFullName}}
 			<a class="muted author-wrapper" title="{{.LatestCommitUser.FullName}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{.LatestCommitUser.FullName}}</strong></a>
 		{{else}}
 			<a class="muted author-wrapper" title="{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}</strong></a>
 		{{end}}
 	{{else}}
 		{{if .LatestCommit.Author}}
-			{{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24 "gt-mr-2"}}
+			{{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24 "tw-mr-1"}}
 			<span class="author-wrapper" title="{{.LatestCommit.Author.Name}}"><strong>{{.LatestCommit.Author.Name}}</strong></span>
 		{{end}}
 	{{end}}
@@ -25,7 +25,7 @@
 	<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject $.Context .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
 		{{if IsMultilineCommitMessage .LatestCommit.Message}}
 			<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
-			<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .LatestCommit.Message ($.Repository.ComposeMetas ctx)}}</pre>
+			<pre class="commit-body tw-hidden">{{RenderCommitBody $.Context .LatestCommit.Message ($.Repository.ComposeMetas ctx)}}</pre>
 		{{end}}
 	</span>
 {{end}}
diff --git a/templates/repo/migrate/codebase.tmpl b/templates/repo/migrate/codebase.tmpl
index a34a039f8f..439a883863 100644
--- a/templates/repo/migrate/codebase.tmpl
+++ b/templates/repo/migrate/codebase.tmpl
@@ -35,22 +35,22 @@
 							<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label>
 							</div>
 						</div>
 					</div>
@@ -90,10 +90,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/git.tmpl b/templates/repo/migrate/git.tmpl
index 7fe4fbc672..db01b8d858 100644
--- a/templates/repo/migrate/git.tmpl
+++ b/templates/repo/migrate/git.tmpl
@@ -64,10 +64,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/gitbucket.tmpl b/templates/repo/migrate/gitbucket.tmpl
index d07351e727..d1f1db99ba 100644
--- a/templates/repo/migrate/gitbucket.tmpl
+++ b/templates/repo/migrate/gitbucket.tmpl
@@ -34,7 +34,7 @@
 						<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 						<div class="ui checkbox">
 							<input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}>
-							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label>
+							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label>
 						</div>
 					</div>
 
@@ -44,29 +44,29 @@
 							<label></label>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="releases" type="checkbox" {{if .releases}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 						</div>
 					</div>
@@ -106,10 +106,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/gitea.tmpl b/templates/repo/migrate/gitea.tmpl
index a40886b7a5..143f220449 100644
--- a/templates/repo/migrate/gitea.tmpl
+++ b/templates/repo/migrate/gitea.tmpl
@@ -30,7 +30,7 @@
 						<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 						<div class="ui checkbox">
 							<input name="wiki" type="checkbox" {{if .wiki}} checked{{end}}>
-							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label>
+							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label>
 						</div>
 					</div>
 
@@ -40,29 +40,29 @@
 							<label></label>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="releases" type="checkbox" {{if .releases}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 						</div>
 					</div>
@@ -102,10 +102,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/github.tmpl b/templates/repo/migrate/github.tmpl
index 07f8216fcb..dfb2b4bc46 100644
--- a/templates/repo/migrate/github.tmpl
+++ b/templates/repo/migrate/github.tmpl
@@ -33,7 +33,7 @@
 						<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 						<div class="ui checkbox">
 							<input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}>
-							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label>
+							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label>
 						</div>
 					</div>
 					<div id="migrate_items">
@@ -42,29 +42,29 @@
 							<label></label>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="releases" type="checkbox" {{if .releases}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 						</div>
 					</div>
@@ -104,10 +104,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/gitlab.tmpl b/templates/repo/migrate/gitlab.tmpl
index 623822df11..76c2828257 100644
--- a/templates/repo/migrate/gitlab.tmpl
+++ b/templates/repo/migrate/gitlab.tmpl
@@ -30,7 +30,7 @@
 						<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 						<div class="ui checkbox">
 							<input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}>
-							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label>
+							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label>
 						</div>
 					</div>
 					<div id="migrate_items">
@@ -39,29 +39,29 @@
 							<label></label>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="releases" type="checkbox" {{if .releases}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 						</div>
 					</div>
@@ -101,10 +101,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/gogs.tmpl b/templates/repo/migrate/gogs.tmpl
index 095efd5d60..b01d0eeb67 100644
--- a/templates/repo/migrate/gogs.tmpl
+++ b/templates/repo/migrate/gogs.tmpl
@@ -30,7 +30,7 @@
 						<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 						<div class="ui checkbox">
 							<input name="wiki" type="checkbox" {{if .wiki}} checked{{end}}>
-							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label>
+							<label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label>
 						</div>
 					</div>
 
@@ -40,18 +40,18 @@
 							<label></label>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 						</div>
 						<!-- Gogs do not support it
@@ -59,11 +59,11 @@
 							<label></label>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="releases" type="checkbox" {{if .releases}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label>
 							</div>
 						</div>
 						-->
@@ -104,10 +104,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}} checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/migrate.tmpl b/templates/repo/migrate/migrate.tmpl
index c686f0b832..c5c697edff 100644
--- a/templates/repo/migrate/migrate.tmpl
+++ b/templates/repo/migrate/migrate.tmpl
@@ -5,22 +5,22 @@
 			{{template "repo/migrate/helper" .}}
 			<div class="ui cards migrate-entries">
 				{{range .Services}}
-					<a class="ui card migrate-entry gt-df gt-ac" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}">
+					<a class="ui card migrate-entry tw-flex tw-items-center" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}">
 						{{if eq .Name "github"}}
-							{{svg "octicon-mark-github" 184 "gt-p-4"}}
+							{{svg "octicon-mark-github" 184 "tw-p-4"}}
 						{{else if eq .Name "gitlab"}}
-							{{svg "gitea-gitlab" 184 "gt-p-4"}}
+							{{svg "gitea-gitlab" 184 "tw-p-4"}}
 						{{else if eq .Name "gitbucket"}}
-							{{svg "gitea-gitbucket" 184 "gt-p-4"}}
+							{{svg "gitea-gitbucket" 184 "tw-p-4"}}
 						{{else}}
 							{{svg (printf "gitea-%s" .Name) 184}}
 						{{end}}
 						<div class="content">
-							<div class="header gt-text-center">
+							<div class="header tw-text-center">
 								{{.Title}}
 							</div>
-							<div class="description gt-text-center">
-								{{(printf "repo.migrate.%s.description" .Name) | ctx.Locale.Tr}}
+							<div class="description tw-text-center">
+								{{ctx.Locale.Tr (printf "repo.migrate.%s.description" .Name)}}
 							</div>
 						</div>
 					</a>
diff --git a/templates/repo/migrate/migrating.tmpl b/templates/repo/migrate/migrating.tmpl
index 48411e2da2..ed03db2ebd 100644
--- a/templates/repo/migrate/migrating.tmpl
+++ b/templates/repo/migrate/migrating.tmpl
@@ -12,7 +12,7 @@
 								<img src="{{AssetUrlPrefix}}/img/loading.png">
 							</div>
 						</div>
-						<div id="repo_migrating_failed_image" class="sixteen wide center aligned centered column gt-hidden">
+						<div id="repo_migrating_failed_image" class="sixteen wide center aligned centered column tw-hidden">
 							<div>
 								<img src="{{AssetUrlPrefix}}/img/failed.png">
 							</div>
@@ -21,14 +21,14 @@
 					<div class="ui stackable middle very relaxed page grid">
 						<div class="sixteen wide center aligned centered column">
 							<div id="repo_migrating_progress">
-								<p>{{ctx.Locale.Tr "repo.migrate.migrating" .CloneAddr | Safe}}</p>
+								<p>{{ctx.Locale.Tr "repo.migrate.migrating" .CloneAddr}}</p>
 								<p id="repo_migrating_progress_message"></p>
 							</div>
-							<div id="repo_migrating_failed" class="gt-hidden">
+							<div id="repo_migrating_failed" class="tw-hidden">
 								{{if .CloneAddr}}
-									<p>{{ctx.Locale.Tr "repo.migrate.migrating_failed" .CloneAddr | Safe}}</p>
+									<p>{{ctx.Locale.Tr "repo.migrate.migrating_failed" .CloneAddr}}</p>
 								{{else}}
-									<p>{{ctx.Locale.Tr "repo.migrate.migrating_failed_no_addr" | Safe}}</p>
+									<p>{{ctx.Locale.Tr "repo.migrate.migrating_failed_no_addr"}}</p>
 								{{end}}
 								<p id="repo_migrating_failed_error"></p>
 							</div>
@@ -40,7 +40,7 @@
 									{{else}}
 										<button class="ui basic show-modal button" data-modal="#cancel-repo-modal">{{ctx.Locale.Tr "cancel"}}</button>
 									{{end}}
-									<button id="repo_migrating_retry" data-migrating-task-retry-url="{{.Link}}/settings/migrate/retry" class="ui basic button gt-hidden">{{ctx.Locale.Tr "retry"}}</button>
+									<button id="repo_migrating_retry" data-migrating-task-retry-url="{{.Link}}/settings/migrate/retry" class="ui basic button tw-hidden">{{ctx.Locale.Tr "retry"}}</button>
 								</div>
 							{{end}}
 						</div>
@@ -57,8 +57,8 @@
 	</div>
 	<div class="content">
 		<div class="ui warning message">
-			{{ctx.Locale.Tr "repo.settings.delete_notices_1" | Safe}}<br>
-			{{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName | Safe}}
+			{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
+			{{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName}}
 			{{if .Repository.NumForks}}<br>
 			{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
 			{{end}}
diff --git a/templates/repo/migrate/onedev.tmpl b/templates/repo/migrate/onedev.tmpl
index b06e6929a1..8b2a2d8730 100644
--- a/templates/repo/migrate/onedev.tmpl
+++ b/templates/repo/migrate/onedev.tmpl
@@ -35,22 +35,22 @@
 							<label>{{ctx.Locale.Tr "repo.migrate_items"}}</label>
 							<div class="ui checkbox">
 								<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label>
 							</div>
 						</div>
 						<div class="inline field">
 							<label></label>
 							<div class="ui checkbox">
 								<input name="issues" type="checkbox" {{if .issues}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label>
 							</div>
 							<div class="ui checkbox">
 								<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label>
 							</div>
 						</div>
 					</div>
@@ -90,10 +90,10 @@
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
 								<input name="private" type="checkbox" checked readonly>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
-								<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+								<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/migrate/options.tmpl b/templates/repo/migrate/options.tmpl
index 1bc30b886d..8a46e5769b 100644
--- a/templates/repo/migrate/options.tmpl
+++ b/templates/repo/migrate/options.tmpl
@@ -14,10 +14,10 @@
 		<input id="lfs" name="lfs" type="checkbox" {{if .lfs}} checked{{end}}>
 		<label>{{ctx.Locale.Tr "repo.migrate_options_lfs"}}</label>
 	</div>
-	<span id="lfs_settings" class="gt-hidden">(<a id="lfs_settings_show" href="#">{{ctx.Locale.Tr "repo.settings.advanced_settings"}}</a>)</span>
+	<span id="lfs_settings" class="tw-hidden">(<a id="lfs_settings_show" href="#">{{ctx.Locale.Tr "repo.settings.advanced_settings"}}</a>)</span>
 </div>
-<div id="lfs_endpoint" class="gt-hidden">
-	<span class="help">{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery" | Str2html}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description.local"}}{{end}}</span>
+<div id="lfs_endpoint" class="tw-hidden">
+	<span class="help">{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description.local"}}{{end}}</span>
 	<div class="inline field {{if .Err_LFSEndpoint}}error{{end}}">
 		<label>{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.label"}}</label>
 		<input name="lfs_endpoint" value="{{.lfs_endpoint}}" placeholder="{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.placeholder"}}">
diff --git a/templates/repo/navbar.tmpl b/templates/repo/navbar.tmpl
new file mode 100644
index 0000000000..b2471dc17e
--- /dev/null
+++ b/templates/repo/navbar.tmpl
@@ -0,0 +1,14 @@
+<div class="ui fluid vertical menu">
+	<a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity">
+		{{ctx.Locale.Tr "repo.activity.navbar.pulse"}}
+	</a>
+	<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
+		{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
+	</a>
+	<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
+		{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
+	</a>
+	<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
+		{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
+	</a>
+</div>
diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl
index 377a7ff79f..05ad7264bf 100644
--- a/templates/repo/projects/view.tmpl
+++ b/templates/repo/projects/view.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
 	{{template "repo/header" .}}
 	<div class="ui container padded">
-		<div class="gt-df gt-sb gt-ac gt-mb-4">
+		<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
 			{{template "repo/issue/navbar" .}}
 			<a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
 		</div>
diff --git a/templates/repo/pulls/fork.tmpl b/templates/repo/pulls/fork.tmpl
index 94de4d78eb..2cf0a85fc8 100644
--- a/templates/repo/pulls/fork.tmpl
+++ b/templates/repo/pulls/fork.tmpl
@@ -37,7 +37,7 @@
 
 					<div class="inline field">
 						<label>{{ctx.Locale.Tr "repo.fork_from"}}</label>
-						<a href="{{.ForkRepo.Link}}" class="gt-dib">{{.ForkRepo.FullName}}</a>
+						<a href="{{.ForkRepo.Link}}" class="tw-inline-block">{{.ForkRepo.FullName}}</a>
 					</div>
 					<div class="inline required field {{if .Err_RepoName}}error{{end}}">
 						<label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label>
@@ -47,7 +47,7 @@
 						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 						<div class="ui disabled checkbox">
 							<input type="checkbox" disabled {{if .IsPrivate}}checked{{end}}>
-							<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label>
+							<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
 						</div>
 						<span class="help">{{ctx.Locale.Tr "repo.fork_visibility_helper"}}</span>
 					</div>
diff --git a/templates/repo/pulls/status.tmpl b/templates/repo/pulls/status.tmpl
index ae508b8fa4..e8636ba1b8 100644
--- a/templates/repo/pulls/status.tmpl
+++ b/templates/repo/pulls/status.tmpl
@@ -2,6 +2,7 @@
 Template Attributes:
 * CommitStatus: summary of all commit status state
 * CommitStatuses: all commit status elements
+* MissingRequiredChecks: commit check contexts that are required by branch protection but not present
 * ShowHideChecks: whether use a button to show/hide the checks
 * is_context_required: Used in pull request commit status check table
 */}}
@@ -9,7 +10,7 @@ Template Attributes:
 {{if .CommitStatus}}
 <div class="commit-status-panel">
 	<div class="ui top attached header commit-status-header">
-		{{if eq .CommitStatus.State "pending"}}
+		{{if or (eq .CommitStatus.State "pending") (.MissingRequiredChecks)}}
 			{{ctx.Locale.Tr "repo.pulls.status_checking"}}
 		{{else if eq .CommitStatus.State "success"}}
 			{{ctx.Locale.Tr "repo.pulls.status_checks_success"}}
@@ -46,6 +47,13 @@ Template Attributes:
 				</div>
 			</div>
 		{{end}}
+		{{range .MissingRequiredChecks}}
+			<div class="commit-status-item">
+				{{svg "octicon-dot-fill" 18 "commit-status icon text yellow"}}
+				<div class="status-context gt-ellipsis">{{.}}</div>
+				<div class="ui label">{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}</div>
+			</div>
+		{{end}}
 	</div>
 </div>
 {{end}}
diff --git a/templates/repo/pulls/tab_menu.tmpl b/templates/repo/pulls/tab_menu.tmpl
index 10bdfdb3de..c0e48928f9 100644
--- a/templates/repo/pulls/tab_menu.tmpl
+++ b/templates/repo/pulls/tab_menu.tmpl
@@ -15,7 +15,7 @@
 			{{ctx.Locale.Tr "repo.pulls.tab_files"}}
 			<span class="ui small label">{{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}}</span>
 		</a>
-		<span class="item gt-ml-auto gt-pr-0 gt-font-bold gt-df gt-ac gt-gap-3">
+		<span class="item tw-ml-auto tw-pr-0 tw-font-bold tw-flex tw-items-center tw-gap-2">
 			<span><span class="text green">{{if .Diff.TotalAddition}}+{{.Diff.TotalAddition}}{{end}}</span> <span class="text red">{{if .Diff.TotalDeletion}}-{{.Diff.TotalDeletion}}{{end}}</span></span>
 			<span class="diff-stats-bar">
 				<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .Diff.TotalAddition "/" "(" .Diff.TotalAddition "+" .Diff.TotalDeletion "+" 0.0 ")"}}%"></div>
diff --git a/templates/repo/pulse.tmpl b/templates/repo/pulse.tmpl
new file mode 100644
index 0000000000..cfb3ec1d3d
--- /dev/null
+++ b/templates/repo/pulse.tmpl
@@ -0,0 +1,227 @@
+<h2 class="ui header activity-header">
+	<span>{{DateTime "long" .DateFrom}} - {{DateTime "long" .DateUntil}}</span>
+	<!-- Period -->
+	<div class="ui floating dropdown jump filter">
+		<div class="ui basic compact button">
+			{{ctx.Locale.Tr "repo.activity.period.filter_label"}} <strong>{{.PeriodText}}</strong>
+			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+		</div>
+		<div class="menu">
+			<a class="{{if eq .Period "daily"}}active {{end}}item" href="{{$.RepoLink}}/activity/daily">{{ctx.Locale.Tr "repo.activity.period.daily"}}</a>
+			<a class="{{if eq .Period "halfweekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/halfweekly">{{ctx.Locale.Tr "repo.activity.period.halfweekly"}}</a>
+			<a class="{{if eq .Period "weekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/weekly">{{ctx.Locale.Tr "repo.activity.period.weekly"}}</a>
+			<a class="{{if eq .Period "monthly"}}active {{end}}item" href="{{$.RepoLink}}/activity/monthly">{{ctx.Locale.Tr "repo.activity.period.monthly"}}</a>
+			<a class="{{if eq .Period "quarterly"}}active {{end}}item" href="{{$.RepoLink}}/activity/quarterly">{{ctx.Locale.Tr "repo.activity.period.quarterly"}}</a>
+			<a class="{{if eq .Period "semiyearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/semiyearly">{{ctx.Locale.Tr "repo.activity.period.semiyearly"}}</a>
+			<a class="{{if eq .Period "yearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/yearly">{{ctx.Locale.Tr "repo.activity.period.yearly"}}</a>
+		</div>
+	</div>
+</h2>
+
+{{if (or (.Permission.CanRead $.UnitTypeIssues) (.Permission.CanRead $.UnitTypePullRequests))}}
+<h4 class="ui top attached header">{{ctx.Locale.Tr "repo.activity.overview"}}</h4>
+<div class="ui attached segment two column grid">
+	{{if .Permission.CanRead $.UnitTypePullRequests}}
+		<div class="column">
+			{{if gt .Activity.ActivePRCount 0}}
+			<div class="stats-table">
+				<a href="#merged-pull-requests" class="table-cell tiny background purple" style="width: {{.Activity.MergedPRPerc}}{{if ne .Activity.MergedPRPerc 0}}%{{end}}"></a>
+				<a href="#proposed-pull-requests" class="table-cell tiny background green"></a>
+			</div>
+			{{else}}
+			<div class="stats-table">
+				<a class="table-cell tiny background light grey"></a>
+			</div>
+			{{end}}
+			{{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount}}
+		</div>
+	{{end}}
+	{{if .Permission.CanRead $.UnitTypeIssues}}
+		<div class="column">
+			{{if gt .Activity.ActiveIssueCount 0}}
+			<div class="stats-table">
+				<a href="#closed-issues" class="table-cell tiny background red" style="width: {{.Activity.ClosedIssuePerc}}{{if ne .Activity.ClosedIssuePerc 0}}%{{end}}"></a>
+				<a href="#new-issues" class="table-cell tiny background green"></a>
+			</div>
+			{{else}}
+			<div class="stats-table">
+				<a class="table-cell tiny background light grey"></a>
+			</div>
+			{{end}}
+			{{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount}}
+		</div>
+	{{end}}
+</div>
+<div class="ui attached segment horizontal segments">
+	{{if .Permission.CanRead $.UnitTypePullRequests}}
+		<a href="#merged-pull-requests" class="ui attached segment text center">
+			<span class="text purple">{{svg "octicon-git-pull-request"}}</span> <strong>{{.Activity.MergedPRCount}}</strong><br>
+			{{ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.merged_prs_count_1" "repo.activity.merged_prs_count_n"}}
+		</a>
+		<a href="#proposed-pull-requests" class="ui attached segment text center">
+			<span class="text green">{{svg "octicon-git-branch"}}</span> <strong>{{.Activity.OpenedPRCount}}</strong><br>
+			{{ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.opened_prs_count_1" "repo.activity.opened_prs_count_n"}}
+		</a>
+	{{end}}
+	{{if .Permission.CanRead $.UnitTypeIssues}}
+		<a href="#closed-issues" class="ui attached segment text center">
+			<span class="text red">{{svg "octicon-issue-closed"}}</span> <strong>{{.Activity.ClosedIssueCount}}</strong><br>
+			{{ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.closed_issues_count_1" "repo.activity.closed_issues_count_n"}}
+		</a>
+		<a href="#new-issues" class="ui attached segment text center">
+			<span class="text green">{{svg "octicon-issue-opened"}}</span> <strong>{{.Activity.OpenedIssueCount}}</strong><br>
+			{{ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.new_issues_count_1" "repo.activity.new_issues_count_n"}}
+		</a>
+	{{end}}
+</div>
+{{end}}
+
+{{if .Permission.CanRead $.UnitTypeCode}}
+	{{if eq .Activity.Code.CommitCountInAllBranches 0}}
+		<div class="ui center aligned segment">
+		<h4 class="ui header">{{ctx.Locale.Tr "repo.activity.no_git_activity"}}</h4>
+		</div>
+	{{end}}
+	{{if gt .Activity.Code.CommitCountInAllBranches 0}}
+		<div class="ui attached segment horizontal segments">
+			<div class="ui attached segment text">
+				{{ctx.Locale.Tr "repo.activity.git_stats_exclude_merges"}}
+				<strong>{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n" .Activity.Code.AuthorCount}}</strong>
+				{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_1" "repo.activity.git_stats_pushed_n"}}
+				<strong>{{ctx.Locale.TrN .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCount}}</strong>
+				{{ctx.Locale.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch}}
+				<strong>{{ctx.Locale.TrN .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCountInAllBranches}}</strong>
+				{{ctx.Locale.Tr "repo.activity.git_stats_push_to_all_branches"}}
+				{{ctx.Locale.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch}}
+				<strong>{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n" .Activity.Code.ChangedFiles}}</strong>
+				{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_1" "repo.activity.git_stats_files_changed_n"}}
+				{{ctx.Locale.Tr "repo.activity.git_stats_additions"}}
+				<strong class="text green">{{ctx.Locale.TrN .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n" .Activity.Code.Additions}}</strong>
+				{{ctx.Locale.Tr "repo.activity.git_stats_and_deletions"}}
+				<strong class="text red">{{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}</strong>.
+			</div>
+			<div class="ui attached segment">
+				<div id="repo-activity-top-authors-chart"></div>
+			</div>
+		</div>
+	{{end}}
+{{end}}
+
+{{if gt .Activity.PublishedReleaseCount 0}}
+	<h4 class="divider divider-text tw-normal-case" id="published-releases">
+		{{svg "octicon-tag" 16 "tw-mr-2"}}
+		{{ctx.Locale.Tr "repo.activity.title.releases_published_by"
+			(ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount)
+			(ctx.Locale.TrN .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.PublishedReleaseAuthorCount)
+		}}
+	</h4>
+	<div class="list">
+		{{range .Activity.PublishedReleases}}
+			<p class="desc">
+				<span class="ui green label">{{ctx.Locale.Tr "repo.activity.published_release_label"}}</span>
+				{{.TagName}}
+				{{if not .IsTag}}
+					<a class="title" href="{{$.RepoLink}}/src/{{.TagName | PathEscapeSegments}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{end}}
+				{{TimeSinceUnix .CreatedUnix ctx.Locale}}
+			</p>
+		{{end}}
+	</div>
+{{end}}
+
+{{if gt .Activity.MergedPRCount 0}}
+	<h4 class="divider divider-text tw-normal-case" id="merged-pull-requests">
+		{{svg "octicon-git-pull-request" 16 "tw-mr-2"}}
+		{{ctx.Locale.Tr "repo.activity.title.prs_merged_by"
+			(ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount)
+			(ctx.Locale.TrN .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.MergedPRAuthorCount)
+		}}
+	</h4>
+	<div class="list">
+		{{range .Activity.MergedPRs}}
+			<p class="desc">
+				<span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span>
+				#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{TimeSinceUnix .MergedUnix ctx.Locale}}
+			</p>
+		{{end}}
+	</div>
+{{end}}
+
+{{if gt .Activity.OpenedPRCount 0}}
+	<h4 class="divider divider-text tw-normal-case" id="proposed-pull-requests">
+		{{svg "octicon-git-branch" 16 "tw-mr-2"}}
+		{{ctx.Locale.Tr "repo.activity.title.prs_opened_by"
+			(ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount)
+			(ctx.Locale.TrN .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedPRAuthorCount)
+		}}
+	</h4>
+	<div class="list">
+		{{range .Activity.OpenedPRs}}
+			<p class="desc">
+				<span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span>
+				#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
+			</p>
+		{{end}}
+	</div>
+{{end}}
+
+{{if gt .Activity.ClosedIssueCount 0}}
+	<h4 class="divider divider-text tw-normal-case" id="closed-issues">
+		{{svg "octicon-issue-closed" 16 "tw-mr-2"}}
+		{{ctx.Locale.Tr "repo.activity.title.issues_closed_from"
+			(ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount)
+			(ctx.Locale.TrN .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.ClosedIssueAuthorCount)
+		}}
+	</h4>
+	<div class="list">
+		{{range .Activity.ClosedIssues}}
+			<p class="desc">
+				<span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span>
+				#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{TimeSinceUnix .ClosedUnix ctx.Locale}}
+			</p>
+		{{end}}
+	</div>
+{{end}}
+
+{{if gt .Activity.OpenedIssueCount 0}}
+	<h4 class="divider divider-text tw-normal-case" id="new-issues">
+		{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
+		{{ctx.Locale.Tr "repo.activity.title.issues_created_by"
+			(ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount)
+			(ctx.Locale.TrN .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedIssueAuthorCount)
+		}}
+	</h4>
+	<div class="list">
+		{{range .Activity.OpenedIssues}}
+			<p class="desc">
+				<span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span>
+				#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{TimeSinceUnix .CreatedUnix ctx.Locale}}
+			</p>
+		{{end}}
+	</div>
+{{end}}
+
+{{if gt .Activity.UnresolvedIssueCount 0}}
+	<h4 class="divider divider-text tw-normal-case" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}">
+		{{svg "octicon-comment-discussion" 16 "tw-mr-2"}}
+		{{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}}
+	</h4>
+	<div class="list">
+		{{range .Activity.UnresolvedIssues}}
+			<p class="desc">
+				<span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span>
+				#{{.Index}}
+				{{if .IsPull}}
+				<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{else}}
+				<a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
+				{{end}}
+				{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
+			</p>
+		{{end}}
+	</div>
+{{end}}
diff --git a/templates/repo/recent_commits.tmpl b/templates/repo/recent_commits.tmpl
new file mode 100644
index 0000000000..5c241d635c
--- /dev/null
+++ b/templates/repo/recent_commits.tmpl
@@ -0,0 +1,9 @@
+{{if .Permission.CanRead $.UnitTypeCode}}
+	<div id="repo-recent-commits-chart"
+		data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.recent_commits.what")}}"
+		data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.recent_commits.what")}}"
+		data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}"
+		data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
+	>
+	</div>
+{{end}}
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index fb2fce2950..3139022bb4 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -5,90 +5,90 @@
 		{{template "base/alert" .}}
 		{{template "repo/release_tag_header" .}}
 		<ul id="release-list">
-			{{range $idx, $release := .Releases}}
+			{{range $idx, $info := .Releases}}
+				{{$release := $info.Release}}
 				<li class="ui grid">
 					<div class="ui four wide column meta">
-							<a class="muted" href="{{if not (and .Sha1 ($.Permission.CanRead $.UnitTypeCode))}}#{{else}}{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}{{end}}" rel="nofollow">{{svg "octicon-tag" 16 "gt-mr-2"}}{{.TagName}}</a>
-							{{if and .Sha1 ($.Permission.CanRead $.UnitTypeCode)}}
-								<a class="muted gt-mono" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "gt-mr-2"}}{{ShortSha .Sha1}}</a>
-								{{template "repo/branch_dropdown" dict "root" $ "release" .}}
-							{{end}}
+						<a class="muted" href="{{if not (and $release.Sha1 ($.Permission.CanRead $.UnitTypeCode))}}#{{else}}{{$.RepoLink}}/src/tag/{{$release.TagName | PathEscapeSegments}}{{end}}" rel="nofollow">{{svg "octicon-tag" 16 "tw-mr-1"}}{{$release.TagName}}</a>
+						{{if and $release.Sha1 ($.Permission.CanRead $.UnitTypeCode)}}
+							<a class="muted tw-font-mono" href="{{$.RepoLink}}/src/commit/{{$release.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha $release.Sha1}}</a>
+							{{template "repo/branch_dropdown" dict "root" $ "release" $release}}
+						{{end}}
 					</div>
 					<div class="ui twelve wide column detail">
-							<div class="gt-df gt-ac gt-sb gt-fw gt-mb-3">
-								<h4 class="release-list-title gt-word-break">
-									<a href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{.Title}}</a>
-									{{if .IsDraft}}
-										<span class="ui yellow label">{{ctx.Locale.Tr "repo.release.draft"}}</span>
-									{{else if .IsPrerelease}}
-										<span class="ui orange label">{{ctx.Locale.Tr "repo.release.prerelease"}}</span>
-									{{else}}
-										<span class="ui green label">{{ctx.Locale.Tr "repo.release.stable"}}</span>
-									{{end}}
-								</h4>
-								<div>
-									{{if $.CanCreateRelease}}
-										<a class="muted" data-tooltip-content="{{ctx.Locale.Tr "repo.release.edit"}}" href="{{$.RepoLink}}/releases/edit/{{.TagName | PathEscapeSegments}}" rel="nofollow">
-											{{svg "octicon-pencil"}}
+						<div class="tw-flex tw-items-center tw-justify-between tw-flex-wrap tw-mb-2">
+							<h4 class="release-list-title gt-word-break">
+								{{if $.PageIsSingleTag}}{{$release.Title}}{{else}}<a href="{{$.RepoLink}}/releases/tag/{{$release.TagName | PathEscapeSegments}}">{{$release.Title}}</a>{{end}}
+								{{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "tw-flex"}}
+								{{if $release.IsDraft}}
+									<span class="ui yellow label">{{ctx.Locale.Tr "repo.release.draft"}}</span>
+								{{else if $release.IsPrerelease}}
+									<span class="ui orange label">{{ctx.Locale.Tr "repo.release.prerelease"}}</span>
+								{{else if (not $release.IsTag)}}
+									<span class="ui green label">{{ctx.Locale.Tr "repo.release.stable"}}</span>
+								{{end}}
+							</h4>
+							<div>
+								{{if and $.CanCreateRelease (not $.PageIsSingleTag)}}
+									<a class="muted" data-tooltip-content="{{ctx.Locale.Tr "repo.release.edit"}}" href="{{$.RepoLink}}/releases/edit/{{$release.TagName | PathEscapeSegments}}" rel="nofollow">
+										{{svg "octicon-pencil"}}
+									</a>
+								{{end}}
+							</div>
+						</div>
+						<p class="text grey">
+							<span class="author">
+							{{if $release.OriginalAuthor}}
+								{{svg (MigrationIcon $release.Repo.GetOriginalURLHostname) 20 "tw-mr-1"}}{{$release.OriginalAuthor}}
+							{{else if $release.Publisher}}
+								{{ctx.AvatarUtils.Avatar $release.Publisher 20 "tw-mr-1"}}
+								<a href="{{$release.Publisher.HomeLink}}">{{$release.Publisher.GetDisplayName}}</a>
+							{{else}}
+								Ghost
+							{{end}}
+							</span>
+							<span class="released">
+								{{ctx.Locale.Tr "repo.released_this"}}
+							</span>
+							{{if $release.CreatedUnix}}
+								<span class="time">{{TimeSinceUnix $release.CreatedUnix ctx.Locale}}</span>
+							{{end}}
+							{{if and (not $release.IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
+								| <span class="ahead"><a href="{{$.RepoLink}}/compare/{{$release.TagName | PathEscapeSegments}}...{{$release.TargetBehind | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.release.ahead.commits" $release.NumCommitsBehind}}</a> {{ctx.Locale.Tr "repo.release.ahead.target" $release.TargetBehind}}</span>
+							{{end}}
+						</p>
+						<div class="markup desc">
+							{{$release.RenderedNote}}
+						</div>
+						<div class="divider"></div>
+						<details class="download" {{if eq $idx 0}}open{{end}}>
+							<summary class="tw-my-4">
+								{{ctx.Locale.Tr "repo.release.downloads"}}
+							</summary>
+							<ul class="list">
+								{{if and (not $.DisableDownloadSourceArchives) (not $release.IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
+									<li>
+										<a class="archive-link" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong></a>
+									</li>
+									<li>
+										<a class="archive-link" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong></a>
+									</li>
+								{{end}}
+								{{range $release.Attachments}}
+									<li>
+										<a target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
+											<strong>{{svg "octicon-package" 16 "tw-mr-1"}}{{.Name}}</strong>
 										</a>
-									{{end}}
-								</div>
-							</div>
-							<p class="text grey">
-								<span class="author">
-								{{if .OriginalAuthor}}
-									{{svg (MigrationIcon .Repo.GetOriginalURLHostname) 20 "gt-mr-2"}}{{.OriginalAuthor}}
-								{{else if .Publisher}}
-									{{ctx.AvatarUtils.Avatar .Publisher 20 "gt-mr-2"}}
-									<a href="{{.Publisher.HomeLink}}">{{.Publisher.GetDisplayName}}</a>
-								{{else}}
-									Ghost
+										<div>
+											<span class="text grey">{{.Size | FileSize}}</span>
+											<span data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .DownloadCount)}}">
+												{{svg "octicon-info"}}
+											</span>
+										</div>
+									</li>
 								{{end}}
-								</span>
-								<span class="released">
-									{{ctx.Locale.Tr "repo.released_this"}}
-								</span>
-								{{if .CreatedUnix}}
-									<span class="time">{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span>
-								{{end}}
-								{{if and (not .IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
-									| <span class="ahead"><a href="{{$.RepoLink}}/compare/{{.TagName | PathEscapeSegments}}...{{.TargetBehind | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.release.ahead.commits" .NumCommitsBehind | Str2html}}</a> {{ctx.Locale.Tr "repo.release.ahead.target" .TargetBehind}}</span>
-								{{end}}
-							</p>
-							<div class="markup desc">
-								{{Str2html .Note}}
-							</div>
-							<div class="divider"></div>
-							<details class="download" {{if eq $idx 0}}open{{end}}>
-								<summary class="gt-my-4">
-									{{ctx.Locale.Tr "repo.release.downloads"}}
-								</summary>
-								<ul class="list">
-									{{if and (not $.DisableDownloadSourceArchives) (not .IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
-										<li>
-											<a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong></a>
-										</li>
-										<li>
-											<a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong></a>
-										</li>
-									{{end}}
-									{{if .Attachments}}
-										{{range .Attachments}}
-											<li>
-												<a target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
-													<strong>{{svg "octicon-package" 16 "gt-mr-2"}}{{.Name}}</strong>
-												</a>
-												<div>
-													<span class="text grey">{{.Size | FileSize}}</span>
-													<span data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .DownloadCount)}}">
-														{{svg "octicon-info"}}
-													</span>
-												</div>
-											</li>
-										{{end}}
-									{{end}}
-								</ul>
-							</details>
+							</ul>
+						</details>
 						<div class="dot"></div>
 					</div>
 				</li>
diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl
index 46b1c9b291..c01f9a421b 100644
--- a/templates/repo/release/new.tmpl
+++ b/templates/repo/release/new.tmpl
@@ -21,7 +21,7 @@
 					{{else}}
 						<input id="tag-name" name="tag_name" value="{{.tag_name}}" aria-label="{{ctx.Locale.Tr "repo.release.tag_name"}}" placeholder="{{ctx.Locale.Tr "repo.release.tag_name"}}" autofocus required maxlength="255">
 						<input id="tag-name-editor" type="hidden" data-existing-tags="{{JsonUtils.EncodeToString .Tags}}" data-tag-helper="{{ctx.Locale.Tr "repo.release.tag_helper"}}" data-tag-helper-new="{{ctx.Locale.Tr "repo.release.tag_helper_new"}}" data-tag-helper-existing="{{ctx.Locale.Tr "repo.release.tag_helper_existing"}}">
-						<div id="tag-target-selector" class="gt-dib">
+						<div id="tag-target-selector" class="tw-inline-block">
 							<span class="at">@</span>
 							<div class="ui selection dropdown">
 								<input type="hidden" name="tag_target" value="{{.tag_target}}">
@@ -39,12 +39,12 @@
 							</div>
 						</div>
 						<div>
-							<span id="tag-helper" class="help gt-mt-3 gt-pb-0">{{ctx.Locale.Tr "repo.release.tag_helper"}}</span>
+							<span id="tag-helper" class="help tw-mt-2 tw-pb-0">{{ctx.Locale.Tr "repo.release.tag_helper"}}</span>
 						</div>
 					{{end}}
 				</div>
 			</div>
-			<div class="eleven wide gt-pt-0">
+			<div class="eleven wide tw-pt-0">
 				<div class="field {{if .Err_Title}}error{{end}}">
 					<input name="title" aria-label="{{ctx.Locale.Tr "repo.release.title"}}" placeholder="{{ctx.Locale.Tr "repo.release.title"}}" value="{{.title}}" autofocus maxlength="255">
 				</div>
@@ -61,10 +61,10 @@
 				</div>
 				{{range .attachments}}
 					<div class="field flex-text-block" id="attachment-{{.ID}}">
-						<div class="flex-text-inline gt-f1">
+						<div class="flex-text-inline tw-flex-1">
 							<input name="attachment-edit-{{.UUID}}"  class="attachment_edit" required value="{{.Name}}">
 							<input name="attachment-del-{{.UUID}}" type="hidden" value="false">
-							<span class="ui text grey gt-whitespace-nowrap">{{.Size | FileSize}}</span>
+							<span class="ui text grey tw-whitespace-nowrap">{{.Size | FileSize}}</span>
 							<span data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .DownloadCount)}}">
 								{{svg "octicon-info"}}
 							</span>
@@ -100,8 +100,8 @@
 						</div>
 					</div>
 					<span class="help">{{ctx.Locale.Tr "repo.release.prerelease_helper"}}</span>
-					<div class="divider gt-mt-0"></div>
-					<div class="gt-df gt-je">
+					<div class="divider tw-mt-0"></div>
+					<div class="tw-flex tw-justify-end">
 						{{if .PageIsEditRelease}}
 							<a class="ui small button" href="{{.RepoLink}}/releases">
 								{{ctx.Locale.Tr "repo.release.cancel"}}
diff --git a/templates/repo/release_tag_header.tmpl b/templates/repo/release_tag_header.tmpl
index f474fb89ea..ab1e58620d 100644
--- a/templates/repo/release_tag_header.tmpl
+++ b/templates/repo/release_tag_header.tmpl
@@ -2,22 +2,22 @@
 {{$canReadCode := $.Permission.CanRead $.UnitTypeCode}}
 
 {{if $canReadReleases}}
-	<div class="gt-df">
-		<div class="gt-f1 gt-df gt-ac">
-			<h2 class="ui compact small menu header small-menu-items">
-				<a class="{{if .PageIsReleaseList}}active {{end}}item" href="{{.RepoLink}}/releases">{{ctx.Locale.PrettyNumber .NumReleases}} {{ctx.Locale.TrN .NumReleases "repo.release" "repo.releases"}}</a>
+	<div class="tw-flex">
+		<div class="tw-flex-1 tw-flex tw-items-center">
+			<h2 class="ui compact small menu small-menu-items">
+				<a class="{{if and .PageIsReleaseList (not .PageIsSingleTag)}}active {{end}}item" href="{{.RepoLink}}/releases">{{ctx.Locale.PrettyNumber .NumReleases}} {{ctx.Locale.TrN .NumReleases "repo.release" "repo.releases"}}</a>
 				{{if $canReadCode}}
-					<a class="{{if .PageIsTagList}}active {{end}}item" href="{{.RepoLink}}/tags">{{ctx.Locale.PrettyNumber .NumTags}} {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}}</a>
+					<a class="{{if or .PageIsTagList .PageIsSingleTag}}active {{end}}item" href="{{.RepoLink}}/tags">{{ctx.Locale.PrettyNumber .NumTags}} {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}}</a>
 				{{end}}
 			</h2>
 		</div>
 		{{if .EnableFeed}}
 			<a class="ui small button" href="{{.RepoLink}}/{{if .PageIsTagList}}tags{{else}}releases{{end}}.rss">
-				{{svg "octicon-rss" 18}} {{ctx.Locale.Tr "rss_feed"}}
+				{{svg "octicon-rss" 16}} {{ctx.Locale.Tr "rss_feed"}}
 			</a>
 		{{end}}
 		{{if and (not .PageIsTagList) .CanCreateRelease}}
-			<a class="ui small primary button" href="{{$.RepoLink}}/releases/new">
+			<a class="ui small primary button" href="{{$.RepoLink}}/releases/new{{if .PageIsSingleTag}}?tag={{.TagName}}{{end}}">
 				{{ctx.Locale.Tr "repo.release.new_release"}}
 			</a>
 		{{end}}
diff --git a/templates/repo/search.tmpl b/templates/repo/search.tmpl
index b616b4de32..3f5b22b0ce 100644
--- a/templates/repo/search.tmpl
+++ b/templates/repo/search.tmpl
@@ -2,72 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository file list">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="ui repo-search">
-			<form class="ui form ignore-dirty" method="get">
-				<div class="ui fluid action input">
-					<input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "repo.search.search_repo"}}">
-					<div class="ui dropdown selection {{if .CodeIndexerUnavailable}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.search.type.tooltip"}}">
-						<input name="t" type="hidden"{{if .CodeIndexerUnavailable}} disabled{{end}} value="{{.queryType}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-						<div class="text">{{ctx.Locale.Tr (printf "repo.search.%s" (or .queryType "fuzzy"))}}</div>
-						<div class="menu">
-							<div class="item" data-value="" data-tooltip-content="{{ctx.Locale.Tr "repo.search.fuzzy.tooltip"}}">{{ctx.Locale.Tr "repo.search.fuzzy"}}</div>
-							<div class="item" data-value="match" data-tooltip-content="{{ctx.Locale.Tr "repo.search.match.tooltip"}}">{{ctx.Locale.Tr "repo.search.match"}}</div>
-						</div>
-					</div>
-					<button class="ui icon button"{{if .CodeIndexerUnavailable}} disabled{{end}} type="submit">{{svg "octicon-search" 16}}</button>
-				</div>
-			</form>
-		</div>
-		{{if .CodeIndexerUnavailable}}
-			<div class="ui error message">
-				<p>{{ctx.Locale.Tr "repo.search.code_search_unavailable"}}</p>
-			</div>
-		{{else if .Keyword}}
-			<h3>
-				{{ctx.Locale.Tr "repo.search.results" (.Keyword|Escape) (.RepoLink|Escape) (.RepoName|Escape) | Str2html}}
-			</h3>
-			{{if .SearchResults}}
-				<div class="flex-text-block gt-fw">
-					{{range $term := .SearchResultLanguages}}
-					<a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label gt-m-0" href="{{$.SourcePath}}/search?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}{{if ne $.queryType ""}}&t={{$.queryType}}{{end}}">
-						<i class="color-icon gt-mr-3" style="background-color: {{$term.Color}}"></i>
-						{{$term.Language}}
-						<div class="detail">{{$term.Count}}</div>
-					</a>
-					{{end}}
-				</div>
-				<div class="repository search">
-					{{range $result := .SearchResults}}
-						<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
-							<h4 class="ui top attached normal header gt-df gt-fw">
-								<span class="file gt-f1">{{.Filename}}</span>
-								<a role="button" class="ui basic tiny button" rel="nofollow" href="{{$.SourcePath}}/src/commit/{{PathEscape $result.CommitID}}/{{PathEscapeSegments .Filename}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
-							</h4>
-							<div class="ui attached table segment">
-								<div class="file-body file-code code-view">
-									<table>
-										<tbody>
-											<tr>
-												<td class="lines-num">
-													{{range .LineNumbers}}
-														<a href="{{$.SourcePath}}/src/commit/{{PathEscape $result.CommitID}}/{{PathEscapeSegments $result.Filename}}#L{{.}}"><span>{{.}}</span></a>
-													{{end}}
-												</td>
-												<td class="lines-code chroma"><code class="code-inner">{{.FormattedLines}}</code></td>
-											</tr>
-										</tbody>
-									</table>
-								</div>
-							</div>
-							{{template "shared/searchbottom" dict "root" $ "result" .}}
-						</div>
-					{{end}}
-				</div>
-				{{template "base/paginate" .}}
-			{{else}}
-				<div>{{ctx.Locale.Tr "repo.search.code_no_results"}}</div>
-			{{end}}
-		{{end}}
+		{{template "shared/search/code/search" .}}
 	</div>
 </div>
 {{template "base/footer" .}}
diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl
index fbdc12defb..52c0c2c800 100644
--- a/templates/repo/settings/branches.tmpl
+++ b/templates/repo/settings/branches.tmpl
@@ -1,7 +1,7 @@
 {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings edit")}}
 	<div class="repo-setting-content">
 		{{if .Repository.IsArchived}}
-			<div class="ui warning message gt-text-center">
+			<div class="ui warning message tw-text-center">
 				{{ctx.Locale.Tr "repo.settings.archive.branchsettings_unavailable"}}
 			</div>
 		{{else}}
@@ -12,11 +12,11 @@
 				<p>
 					{{ctx.Locale.Tr "repo.settings.default_branch_desc"}}
 				</p>
-				<form class="gt-df" action="{{.Link}}" method="post">
+				<form class="tw-flex" action="{{.Link}}" method="post">
 					{{.CsrfTokenHtml}}
 					<input type="hidden" name="action" value="default_branch">
 					{{if not .Repository.IsEmpty}}
-						<div class="ui dropdown selection gt-f1 gt-mr-3 gt-max-width-24rem">
+						<div class="ui dropdown selection search tw-flex-1 tw-mr-2 tw-max-w-96">
 							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 							<input type="hidden" name="branch" value="{{.Repository.DefaultBranch}}">
 							<div class="default text">{{.Repository.DefaultBranch}}</div>
@@ -41,7 +41,7 @@
 			<div class="ui attached segment">
 				<div class="flex-list">
 					{{range .ProtectedBranches}}
-						<div class="flex-item gt-ac">
+						<div class="flex-item tw-items-center">
 							<div class="flex-item-main">
 								<div class="flex-item-title">
 									<div class="ui basic primary label">{{.RuleName}}</div>
diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl
index 19abf1bee8..2a4ec577e7 100644
--- a/templates/repo/settings/collaboration.tmpl
+++ b/templates/repo/settings/collaboration.tmpl
@@ -7,7 +7,7 @@
 		<div class="ui attached segment">
 			<div class="flex-list">
 				{{range .Collaborators}}
-					<div class="flex-item gt-ac">
+					<div class="flex-item tw-items-center">
 						<div class="flex-item-leading">
 							<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 32}}</a>
 						</div>
@@ -41,8 +41,8 @@
 		<div class="ui bottom attached segment">
 			<form class="ui form" id="repo-collab-form" action="{{.Link}}" method="post">
 				{{.CsrfTokenHtml}}
-				<div id="search-user-box" class="ui search input gt-vm">
-					<input class="prompt" name="collaborator" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" autofocus required>
+				<div id="search-user-box" class="ui search input tw-align-middle">
+					<input class="prompt" name="collaborator" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" autofocus required>
 				</div>
 				<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_collaborator"}}</button>
 			</form>
@@ -89,8 +89,8 @@
 			{{if $allowedToChangeTeams}}
 				<form class="ui form" id="repo-collab-team-form" action="{{.Link}}/team" method="post">
 					{{.CsrfTokenHtml}}
-					<div id="search-team-box" class="ui search input gt-vm" data-org-name="{{.OrgName}}">
-						<input class="prompt" name="team" placeholder="{{ctx.Locale.Tr "repo.settings.search_team"}}" autocomplete="off" autofocus required>
+					<div id="search-team-box" class="ui search input tw-align-middle" data-org-name="{{.OrgName}}">
+						<input class="prompt" name="team" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" autofocus required>
 					</div>
 					<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_team"}}</button>
 				</form>
diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl
index c5d2d2a04a..da1a321785 100644
--- a/templates/repo/settings/deploy_keys.tmpl
+++ b/templates/repo/settings/deploy_keys.tmpl
@@ -11,18 +11,18 @@
 			</div>
 		</h4>
 		<div class="ui attached segment">
-			<div class="{{if not .HasError}}gt-hidden{{end}} gt-mb-4" id="add-deploy-key-panel">
+			<div class="{{if not .HasError}}tw-hidden{{end}} tw-mb-4" id="add-deploy-key-panel">
 				<form class="ui form" action="{{.Link}}" method="post">
 					{{.CsrfTokenHtml}}
 					<div class="field">
 						{{ctx.Locale.Tr "repo.settings.deploy_key_desc"}}
 					</div>
 					<div class="field {{if .Err_Title}}error{{end}}">
-						<label for="title">{{ctx.Locale.Tr "repo.settings.title"}}</label>
+						<label for="ssh-key-title">{{ctx.Locale.Tr "repo.settings.title"}}</label>
 						<input id="ssh-key-title" name="title" value="{{.title}}" autofocus required>
 					</div>
 					<div class="field {{if .Err_Content}}error{{end}}">
-						<label for="content">{{ctx.Locale.Tr "repo.settings.deploy_key_content"}}</label>
+						<label for="ssh-key-content">{{ctx.Locale.Tr "repo.settings.deploy_key_content"}}</label>
 						<textarea id="ssh-key-content" name="content" placeholder="{{ctx.Locale.Tr "settings.key_content_ssh_placeholder"}}" required>{{.content}}</textarea>
 					</div>
 					<div class="field">
@@ -31,7 +31,7 @@
 							<label for="is_writable">
 								{{ctx.Locale.Tr "repo.settings.is_writable"}}
 							</label>
-							<small style="padding-left: 26px;">{{ctx.Locale.Tr "repo.settings.is_writable_info" | Str2html}}</small>
+							<small class="tw-pl-[26px]">{{ctx.Locale.Tr "repo.settings.is_writable_info"}}</small>
 						</div>
 					</div>
 					<button class="ui primary button">
@@ -55,7 +55,7 @@
 									{{.Fingerprint}}
 								</div>
 								<div class="flex-item-body">
-									<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} —  {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}} - <span>{{ctx.Locale.Tr "settings.can_read_info"}}{{if not .IsReadOnly}} / {{ctx.Locale.Tr "settings.can_write_info"}} {{end}}</span></i>
+									<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} —  {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}} - <span>{{ctx.Locale.Tr "settings.can_read_info"}}{{if not .IsReadOnly}} / {{ctx.Locale.Tr "settings.can_write_info"}} {{end}}</span></i>
 								</div>
 							</div>
 							<div class="flex-item-trailing">
diff --git a/templates/repo/settings/githook_edit.tmpl b/templates/repo/settings/githook_edit.tmpl
index db8982a282..e20f51b922 100644
--- a/templates/repo/settings/githook_edit.tmpl
+++ b/templates/repo/settings/githook_edit.tmpl
@@ -14,7 +14,7 @@
 					</div>
 					<div class="field">
 						<label for="content">{{ctx.Locale.Tr "repo.settings.githook_content"}}</label>
-						<textarea id="content" name="content" class="gt-hidden">{{if .IsActive}}{{.Content}}{{else}}{{.Sample}}{{end}}</textarea>
+						<textarea id="content" name="content" class="tw-hidden">{{if .IsActive}}{{.Content}}{{else}}{{.Sample}}{{end}}</textarea>
 						<div class="editor-loading is-loading"></div>
 					</div>
 					<div class="inline field">
diff --git a/templates/repo/settings/githooks.tmpl b/templates/repo/settings/githooks.tmpl
index 389d381f30..1a603f9fe8 100644
--- a/templates/repo/settings/githooks.tmpl
+++ b/templates/repo/settings/githooks.tmpl
@@ -6,13 +6,13 @@
 		<div class="ui attached segment">
 			<div class="ui list">
 				<div class="item">
-					{{ctx.Locale.Tr "repo.settings.githooks_desc" | Str2html}}
+					{{ctx.Locale.Tr "repo.settings.githooks_desc"}}
 				</div>
 				{{range .Hooks}}
 					<div class="item truncated-item-container">
-						<span class="text {{if .IsActive}}green{{else}}grey{{end}} gt-mr-3">{{svg "octicon-dot-fill" 22}}</span>
-						<span class="text truncate gt-f1 gt-mr-3">{{.Name}}</span>
-						<a class="muted gt-float-right gt-p-3" href="{{$.RepoLink}}/settings/hooks/git/{{.Name|PathEscape}}">
+						<span class="text {{if .IsActive}}green{{else}}grey{{end}} tw-mr-2">{{svg "octicon-dot-fill" 22}}</span>
+						<span class="text truncate tw-flex-1 tw-mr-2">{{.Name}}</span>
+						<a class="muted tw-float-right tw-p-2" href="{{$.RepoLink}}/settings/hooks/git/{{.Name|PathEscape}}">
 							{{svg "octicon-pencil"}}
 						</a>
 					</div>
diff --git a/templates/repo/settings/lfs.tmpl b/templates/repo/settings/lfs.tmpl
index dca4d1f1ce..e0864ff221 100644
--- a/templates/repo/settings/lfs.tmpl
+++ b/templates/repo/settings/lfs.tmpl
@@ -12,7 +12,7 @@
 				{{range .LFSFiles}}
 					<tr>
 						<td>
-							<a href="{{$.Link}}/show/{{.Oid}}" title="{{.Oid}}" class="ui brown button gt-mono">
+							<a href="{{$.Link}}/show/{{.Oid}}" title="{{.Oid}}" class="ui brown button tw-font-mono">
 								{{ShortSha .Oid}}
 							</a>
 						</td>
diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl
index 0aeb2af178..cb65236f23 100644
--- a/templates/repo/settings/lfs_file.tmpl
+++ b/templates/repo/settings/lfs_file.tmpl
@@ -5,7 +5,7 @@
 				<a href="{{.LFSFilesLink}}">{{ctx.Locale.Tr "repo.settings.lfs"}}</a> / <span class="truncate sha">{{.LFSFile.Oid}}</span>
 				<div class="ui right">
 					{{if .EscapeStatus.Escaped}}
-						<a class="ui tiny basic button unescape-button gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
+						<a class="ui tiny basic button unescape-button tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
 						<a class="ui tiny basic button escape-button">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a>
 					{{end}}
 					<a class="ui primary tiny button" href="{{.LFSFilesLink}}/find?oid={{.LFSFile.Oid}}&size={{.LFSFile.Size}}">{{ctx.Locale.Tr "repo.settings.lfs_findcommits"}}</a>
@@ -14,10 +14,12 @@
 			<div class="ui attached table unstackable segment">
 				{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
 				<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextFile}} code-view{{end}}">
-					{{if .IsMarkup}}
-						{{if .FileContent}}{{.FileContent | Safe}}{{end}}
+					{{if .IsFileTooLarge}}
+						{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
+					{{else if .IsMarkup}}
+						{{if .FileContent}}{{.FileContent | SafeHTML}}{{end}}
 					{{else if .IsPlainText}}
-						<pre>{{if .FileContent}}{{.FileContent | Safe}}{{end}}</pre>
+						<pre>{{if .FileContent}}{{.FileContent | SafeHTML}}{{end}}</pre>
 					{{else if not .IsTextFile}}
 						<div class="view-raw">
 							{{if .IsImageFile}}
@@ -33,19 +35,15 @@
 							{{else if .IsPDFFile}}
 								<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "diff.view_file"}}"></div>
 							{{else}}
-								<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
+								<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
 							{{end}}
 						</div>
 					{{else if .FileSize}}
 						<table>
 							<tbody>
 								<tr>
-								{{if .IsFileTooLarge}}
-									<td><strong>{{ctx.Locale.Tr "repo.file_too_large"}}</strong></td>
-								{{else}}
 									<td class="lines-num">{{.LineNums}}</td>
 									<td class="lines-code"><pre><code class="{{.HighlightClass}}"><ol>{{.FileContent}}</ol></code></pre></td>
-								{{end}}
 								</tr>
 							</tbody>
 						</table>
diff --git a/templates/repo/settings/lfs_file_find.tmpl b/templates/repo/settings/lfs_file_find.tmpl
index fea9aa323f..809a028b2c 100644
--- a/templates/repo/settings/lfs_file_find.tmpl
+++ b/templates/repo/settings/lfs_file_find.tmpl
@@ -14,7 +14,7 @@
 							</td>
 							<td class="message">
 								<span class="truncate">
-									<a href="{{$.RepoLink}}/commit/{{.SHA}}" title="{{.Summary | RenderEmojiPlain}}">
+									<a href="{{$.RepoLink}}/commit/{{.SHA}}" title="{{.Summary}}">
 										{{.Summary | RenderEmoji $.Context}}
 									</a>
 								</span>
diff --git a/templates/repo/settings/lfs_pointers.tmpl b/templates/repo/settings/lfs_pointers.tmpl
index fdc6b536c2..a0bb8c46f0 100644
--- a/templates/repo/settings/lfs_pointers.tmpl
+++ b/templates/repo/settings/lfs_pointers.tmpl
@@ -32,19 +32,19 @@
 					{{range .Pointers}}
 						<tr>
 							<td>
-								<a href="{{$.RepoLink}}/raw/blob/{{.SHA}}" rel="nofollow" target="_blank" title="{{.SHA}}" class="ui button gt-mono">
+								<a href="{{$.RepoLink}}/raw/blob/{{.SHA}}" rel="nofollow" target="_blank" title="{{.SHA}}" class="ui button tw-font-mono">
 									{{ShortSha .SHA}}
 								</a>
 							</td>
 							<td>
-								<a {{if and .Exists .InRepo}}href="{{$.LFSFilesLink}}/show/{{.Oid}}" rel="nofollow" target="_blank"{{end}} title="{{.Oid}}" class="ui brown button gt-mono">
+								<a {{if and .Exists .InRepo}}href="{{$.LFSFilesLink}}/show/{{.Oid}}" rel="nofollow" target="_blank"{{end}} title="{{.Oid}}" class="ui brown button tw-font-mono">
 									{{ShortSha .Oid}}
 								</a>
 							</td>
 							<td>{{if .InRepo}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 							<td>{{if .Exists}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 							<td>{{if .Accessible}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
-							<td class="gt-text-right">
+							<td class="tw-text-right">
 								<a class="ui primary button" href="{{$.LFSFilesLink}}/find?oid={{.Oid}}&size={{.Size}}&sha={{.SHA}}">{{ctx.Locale.Tr "repo.settings.lfs_findcommits"}}</a>
 							</td>
 						</tr>
diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl
index 3bef0fa4c1..0b0ef0b6e8 100644
--- a/templates/repo/settings/navbar.tmpl
+++ b/templates/repo/settings/navbar.tmpl
@@ -12,10 +12,12 @@
 				{{ctx.Locale.Tr "repo.settings.hooks"}}
 			</a>
 		{{end}}
-		{{if and (.Repository.UnitEnabled $.Context $.UnitTypeCode) (not .Repository.IsEmpty)}}
-			<a class="{{if .PageIsSettingsBranches}}active {{end}}item" href="{{.RepoLink}}/settings/branches">
-				{{ctx.Locale.Tr "repo.settings.branches"}}
-			</a>
+		{{if .Repository.UnitEnabled $.Context $.UnitTypeCode}}
+			{{if not .Repository.IsEmpty}}
+				<a class="{{if .PageIsSettingsBranches}}active {{end}}item" href="{{.RepoLink}}/settings/branches">
+					{{ctx.Locale.Tr "repo.settings.branches"}}
+				</a>
+			{{end}}
 			<a class="{{if .PageIsSettingsTags}}active {{end}}item" href="{{.RepoLink}}/settings/tags">
 				{{ctx.Locale.Tr "repo.settings.tags"}}
 			</a>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index dfb909e743..b8fa4759b1 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -32,7 +32,7 @@
 							{{else}}
 							<input name="private" type="checkbox" {{if .Repository.IsPrivate}}checked{{end}}{{if and $.ForcePrivate .Repository.IsPrivate}} readonly{{end}}>
 							{{end}}
-							<label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}} {{if .Repository.NumForks}}<span class="text red">{{ctx.Locale.Tr "repo.visibility_fork_helper"}}</span>{{end}}</label>
+							<label>{{ctx.Locale.Tr "repo.visibility_helper"}} {{if .Repository.NumForks}}<span class="text red">{{ctx.Locale.Tr "repo.visibility_fork_helper"}}</span>{{end}}</label>
 						</div>
 					</div>
 				{{end}}
@@ -80,7 +80,7 @@
 			</h4>
 			<div class="ui attached segment">
 				{{if .Repository.IsArchived}}
-					<div class="ui warning message gt-text-center">
+					<div class="ui warning message tw-text-center">
 						{{ctx.Locale.Tr "repo.settings.archive.mirrors_unavailable"}}
 					</div>
 				{{else}}
@@ -110,19 +110,18 @@
 					<table class="ui table">
 						<thead>
 							<tr>
-								<th style="width:40%">{{ctx.Locale.Tr "repo.settings.mirror_settings.mirrored_repository"}}</th>
+								<th class="tw-w-2/5">{{ctx.Locale.Tr "repo.settings.mirror_settings.mirrored_repository"}}</th>
 								<th>{{ctx.Locale.Tr "repo.settings.mirror_settings.direction"}}</th>
 								<th>{{ctx.Locale.Tr "repo.settings.mirror_settings.last_update"}}</th>
 								<th></th>
 							</tr>
 						</thead>
-						{{end}}
 						{{if $modifyBrokenPullMirror}}
 							{{/* even if a repo is a pull mirror (IsMirror=true), the PullMirror might still be nil if the mirror migration is broken */}}
 							<tbody>
 								<tr>
 									<td colspan="4">
-										<div class="text red gt-py-4 gt-border-secondary-bottom">{{ctx.Locale.Tr "repo.settings.mirror_settings.direction.pull"}}: {{ctx.Locale.Tr "error.occurred"}}</div>
+										<div class="text red tw-py-4">{{ctx.Locale.Tr "repo.settings.mirror_settings.direction.pull"}}: {{ctx.Locale.Tr "error.occurred"}}</div>
 									</td>
 								</tr>
 							</tbody>
@@ -133,7 +132,7 @@
 								<td>{{ctx.Locale.Tr "repo.settings.mirror_settings.direction.pull"}}</td>
 								<td>{{DateTime "full" .PullMirror.UpdatedUnix}}</td>
 								<td class="right aligned">
-									<form method="post" class="gt-dib">
+									<form method="post" class="tw-inline-block">
 										{{.CsrfTokenHtml}}
 										<input type="hidden" name="action" value="mirror-sync">
 										<button class="ui primary tiny button inline text-thin">{{ctx.Locale.Tr "repo.settings.sync_mirror"}}</button>
@@ -164,10 +163,10 @@
 											<p class="help">{{ctx.Locale.Tr "repo.mirror_address_desc"}}</p>
 										</div>
 										<details class="ui optional field" {{if or .Err_Auth $address.Username}}open{{end}}>
-											<summary class="gt-p-2">
+											<summary class="tw-p-1">
 												{{ctx.Locale.Tr "repo.need_auth"}}
 											</summary>
-											<div class="gt-p-2">
+											<div class="tw-p-1">
 												<div class="inline field {{if .Err_Auth}}error{{end}}">
 													<label for="mirror_username">{{ctx.Locale.Tr "username"}}</label>
 													<input id="mirror_username" name="mirror_username" value="{{$address.Username}}" {{if not .mirror_username}}data-need-clear="true"{{end}}>
@@ -191,7 +190,7 @@
 										<div class="field {{if .Err_LFSEndpoint}}error{{end}}">
 											<label for="mirror_lfs_endpoint">{{ctx.Locale.Tr "repo.mirror_lfs_endpoint"}}</label>
 											<input id="mirror_lfs_endpoint" name="mirror_lfs_endpoint" value="{{.PullMirror.LFSEndpoint}}" placeholder="{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.placeholder"}}">
-											<p class="help">{{ctx.Locale.Tr "repo.mirror_lfs_endpoint_desc" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery" | Str2html}}</p>
+											<p class="help">{{ctx.Locale.Tr "repo.mirror_lfs_endpoint_desc" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery"}}</p>
 										</div>
 										{{end}}
 										<div class="field">
@@ -201,13 +200,14 @@
 								</td>
 							</tr>
 						</tbody>
+						{{end}}{{/* end if: $modifyBrokenPullMirror / $isWorkingPullMirror */}}
 					</table>
-					{{end}}{{/* end if: IsMirror */}}
+					{{end}}{{/* end if .Repository.IsMirror */}}
 
 					<table class="ui table">
 						<thead>
 							<tr>
-								<th style="width:40%">{{ctx.Locale.Tr "repo.settings.mirror_settings.pushed_repository"}}</th>
+								<th class="tw-w-2/5">{{ctx.Locale.Tr "repo.settings.mirror_settings.pushed_repository"}}</th>
 								<th>{{ctx.Locale.Tr "repo.settings.mirror_settings.direction"}}</th>
 								<th>{{ctx.Locale.Tr "repo.settings.mirror_settings.last_update"}}</th>
 								<th></th>
@@ -230,13 +230,13 @@
 									>
 										{{svg "octicon-pencil" 14}}
 									</button>
-									<form method="post" class="gt-dib">
+									<form method="post" class="tw-inline-block">
 										{{$.CsrfTokenHtml}}
 										<input type="hidden" name="action" value="push-mirror-sync">
 										<input type="hidden" name="push_mirror_id" value="{{.ID}}">
 										<button class="ui primary tiny button" data-tooltip-content="{{ctx.Locale.Tr "repo.settings.sync_mirror"}}">{{svg "octicon-sync" 14}}</button>
 									</form>
-									<form method="post" class="gt-dib">
+									<form method="post" class="tw-inline-block">
 										{{$.CsrfTokenHtml}}
 										<input type="hidden" name="action" value="push-mirror-remove">
 										<input type="hidden" name="push_mirror_id" value="{{.ID}}">
@@ -262,10 +262,10 @@
 												<p class="help">{{ctx.Locale.Tr "repo.mirror_address_desc"}}</p>
 											</div>
 											<details class="ui optional field" {{if or .Err_PushMirrorAuth .push_mirror_username}}open{{end}}>
-												<summary class="gt-p-2">
+												<summary class="tw-p-1">
 													{{ctx.Locale.Tr "repo.need_auth"}}
 												</summary>
-												<div class="gt-p-2">
+												<div class="tw-p-1">
 													<div class="inline field {{if .Err_PushMirrorAuth}}error{{end}}">
 														<label for="push_mirror_username">{{ctx.Locale.Tr "username"}}</label>
 														<input id="push_mirror_username" name="push_mirror_username" value="{{.push_mirror_username}}">
@@ -335,13 +335,17 @@
 							<label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label>
 						</div>
 					</div>
+					<div class="inline field tw-pl-4">
+						<label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label>
+						<input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}">
+					</div>
 					<div class="field">
 						<div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
 							<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="true" data-target="#external_wiki_box" {{if .Repository.UnitEnabled $.Context $.UnitTypeExternalWiki}}checked{{end}}>
 							<label>{{ctx.Locale.Tr "repo.settings.use_external_wiki"}}</label>
 						</div>
 					</div>
-					<div class="field gt-pl-4 {{if not (.Repository.UnitEnabled $.Context $.UnitTypeExternalWiki)}}disabled{{end}}" id="external_wiki_box">
+					<div class="field tw-pl-4 {{if not (.Repository.UnitEnabled $.Context $.UnitTypeExternalWiki)}}disabled{{end}}" id="external_wiki_box">
 						<label for="external_wiki_url">{{ctx.Locale.Tr "repo.settings.external_wiki_url"}}</label>
 						<input id="external_wiki_url" name="external_wiki_url" type="url" value="{{(.Repository.MustGetUnit $.Context $.UnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}">
 						<p class="help">{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}</p>
@@ -368,7 +372,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.use_internal_issue_tracker"}}</label>
 						</div>
 					</div>
-					<div class="field gt-pl-4 {{if (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box">
+					<div class="field tw-pl-4 {{if (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box">
 						{{if .Repository.CanEnableTimetracker}}
 							<div class="field">
 								<div class="ui checkbox">
@@ -400,7 +404,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.use_external_issue_tracker"}}</label>
 						</div>
 					</div>
-					<div class="field gt-pl-4 {{if not (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}}disabled{{end}}" id="external_issue_box">
+					<div class="field tw-pl-4 {{if not (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}}disabled{{end}}" id="external_issue_box">
 						<div class="field">
 							<label for="external_tracker_url">{{ctx.Locale.Tr "repo.settings.external_tracker_url"}}</label>
 							<input id="external_tracker_url" name="external_tracker_url" type="url" value="{{(.Repository.MustGetUnit $.Context $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerURL}}">
@@ -409,7 +413,7 @@
 						<div class="field">
 							<label for="tracker_url_format">{{ctx.Locale.Tr "repo.settings.tracker_url_format"}}</label>
 							<input id="tracker_url_format" name="tracker_url_format" type="url" value="{{(.Repository.MustGetUnit $.Context $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerFormat}}" placeholder="https://github.com/{user}/{repo}/issues/{index}">
-							<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc" | Str2html}}</p>
+							<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc"}}</p>
 						</div>
 						<div class="inline fields">
 							<label for="issue_style">{{ctx.Locale.Tr "repo.settings.tracker_issue_style"}}</label>
@@ -437,7 +441,7 @@
 						<div class="field {{if ne $externalTrackerStyle "regexp"}}disabled{{end}}" id="tracker-issue-style-regex-box">
 							<label for="external_tracker_regexp_pattern">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern"}}</label>
 							<input id="external_tracker_regexp_pattern" name="external_tracker_regexp_pattern" value="{{(.Repository.MustGetUnit $.Context $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerRegexpPattern}}">
-							<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc" | Str2html}}</p>
+							<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc"}}</p>
 						</div>
 					</div>
 				</div>
@@ -446,13 +450,45 @@
 
 				{{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}}
 				{{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}}
+				{{$projectsUnit := .Repository.MustGetUnit $.Context $.UnitTypeProjects}}
 				<div class="inline field">
 					<label>{{ctx.Locale.Tr "repo.project_board"}}</label>
 					<div class="ui checkbox{{if $isProjectsGlobalDisabled}} disabled{{end}}"{{if $isProjectsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
-						<input class="enable-system" name="enable_projects" type="checkbox" {{if $isProjectsEnabled}}checked{{end}}>
+						<input class="enable-system" name="enable_projects" type="checkbox" data-target="#projects_box" {{if $isProjectsEnabled}}checked{{end}}>
 						<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
 					</div>
 				</div>
+				<div class="field {{if not $isProjectsEnabled}} disabled{{end}} tw-pl-4" id="projects_box">
+					<p>
+						{{ctx.Locale.Tr "repo.settings.projects_mode_desc"}}
+					</p>
+					<div class="ui dropdown selection">
+						<select name="projects_mode">
+							<option value="repo" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</option>
+							<option value="owner" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</option>
+							<option value="all" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</option>
+						</select>
+						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+						<div class="default text">
+							{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}
+								{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}
+							{{end}}
+							{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}
+								{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}
+							{{end}}
+							{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}
+								{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}
+							{{end}}
+						</div>
+						<div class="menu">
+							<div class="item" data-value="repo">{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</div>
+							<div class="item" data-value="owner">{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</div>
+							<div class="item" data-value="all">{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</div>
+						</div>
+					</div>
+				</div>
+
+				<div class="divider"></div>
 
 				{{$isReleasesEnabled := .Repository.UnitEnabled $.Context $.UnitTypeReleases}}
 				{{$isReleasesGlobalDisabled := .UnitTypeReleases.UnitGlobalDisabled}}
@@ -528,6 +564,12 @@
 								<label>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</label>
 							</div>
 						</div>
+						<div class="field">
+							<div class="ui checkbox">
+								<input name="pulls_allow_fast_forward_only" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowFastForwardOnly)}}checked{{end}}>
+								<label>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</label>
+							</div>
+						</div>
 						<div class="field">
 							<div class="ui checkbox">
 								<input name="pulls_allow_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowManualMerge)}}checked{{end}}>
@@ -545,6 +587,7 @@
 									<option value="rebase" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</option>
 									<option value="rebase-merge" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase-merge")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</option>
 									<option value="squash" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</option>
+									<option value="fast-forward-only" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</option>
 								</select>{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 								<div class="default text">
 									{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "merge")}}
@@ -559,12 +602,16 @@
 									{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}
 										{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}
 									{{end}}
+									{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}
+										{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}
+									{{end}}
 								</div>
 								<div class="menu">
 									<div class="item" data-value="merge">{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</div>
 									<div class="item" data-value="rebase">{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</div>
 									<div class="item" data-value="rebase-merge">{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</div>
 									<div class="item" data-value="squash">{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</div>
+									<div class="item" data-value="fast-forward-only">{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</div>
 								</div>
 							</div>
 						</div>
@@ -790,7 +837,7 @@
 					</div>
 				</div>
 				{{if not .Repository.IsMirror}}
-					<div class="flex-item gt-ac">
+					<div class="flex-item tw-items-center">
 						<div class="flex-item-main">
 							{{if .Repository.IsArchived}}
 								<div class="flex-item-title">{{ctx.Locale.Tr "repo.settings.unarchive.header"}}</div>
@@ -922,8 +969,8 @@
 		</div>
 		<div class="content">
 			<div class="ui warning message">
-				{{ctx.Locale.Tr "repo.settings.delete_notices_1" | Safe}}<br>
-				{{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName | Safe}}
+				{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
+				{{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName}}
 				{{if .Repository.NumForks}}<br>
 				{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
 				{{end}}
@@ -957,8 +1004,8 @@
 		</div>
 		<div class="content">
 			<div class="ui warning message">
-				{{ctx.Locale.Tr "repo.settings.delete_notices_1" | Safe}}<br>
-				{{ctx.Locale.Tr "repo.settings.wiki_delete_notices_1" .Repository.Name | Safe}}
+				{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
+				{{ctx.Locale.Tr "repo.settings.wiki_delete_notices_1" .Repository.Name}}
 			</div>
 			<form class="ui form" action="{{.Link}}" method="post">
 				{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl
index 9c0fbddf06..fec4d7c8d4 100644
--- a/templates/repo/settings/protected_branch.tmpl
+++ b/templates/repo/settings/protected_branch.tmpl
@@ -2,7 +2,7 @@
 	<div class="repo-setting-content">
 		<form class="ui form" action="{{.Link}}" method="post">
 			<h4 class="ui top attached header">
-				{{ctx.Locale.Tr "repo.settings.branch_protection" (.Rule.RuleName|Escape) | Str2html}}
+				{{ctx.Locale.Tr "repo.settings.branch_protection" .Rule.RuleName}}
 			</h4>
 			<div class="ui attached segment branch-protection">
 				<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.protect_patterns"}}</h5>
@@ -10,17 +10,17 @@
 					<label>{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern"}}</label>
 					<input name="rule_name" type="text" value="{{.Rule.RuleName}}">
 					<input name="rule_id" type="hidden" value="{{.Rule.ID}}">
-					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern_desc" | Safe}}</p>
+					<p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern_desc"}}</p>
 				</div>
 				<div class="field">
 					<label>{{ctx.Locale.Tr "repo.settings.protect_protected_file_patterns"}}</label>
 					<input name="protected_file_patterns" type="text" value="{{.Rule.ProtectedFilePatterns}}">
-					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_protected_file_patterns_desc" | Safe}}</p>
+					<p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_protected_file_patterns_desc"}}</p>
 				</div>
 				<div class="field">
 					<label>{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns"}}</label>
 					<input name="unprotected_file_patterns" type="text" value="{{.Rule.UnprotectedFilePatterns}}">
-					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns_desc" | Safe}}</p>
+					<p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns_desc"}}</p>
 				</div>
 
 				{{.CsrfTokenHtml}}
@@ -52,7 +52,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.protect_whitelist_users"}}</label>
 							<div class="ui multiple search selection dropdown">
 								<input type="hidden" name="whitelist_users" value="{{.whitelist_users}}">
-								<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_users"}}</div>
+								<div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div>
 								<div class="menu">
 									{{range .Users}}
 										<div class="item" data-value="{{.ID}}">
@@ -67,7 +67,7 @@
 								<label>{{ctx.Locale.Tr "repo.settings.protect_whitelist_teams"}}</label>
 								<div class="ui multiple search selection dropdown">
 									<input type="hidden" name="whitelist_teams" value="{{.whitelist_teams}}">
-									<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_teams"}}</div>
+									<div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div>
 									<div class="menu">
 										{{range .Teams}}
 											<div class="item" data-value="{{.ID}}">
@@ -98,7 +98,7 @@
 				<div class="field">
 					<label>{{ctx.Locale.Tr "repo.settings.protect_required_approvals"}}</label>
 					<input name="required_approvals" type="number" value="{{.Rule.RequiredApprovals}}">
-					<p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_required_approvals_desc"}}</p>
+					<p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_required_approvals_desc"}}</p>
 				</div>
 				<div class="grouped fields">
 					<div class="field">
@@ -113,7 +113,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.protect_approvals_whitelist_users"}}</label>
 							<div class="ui multiple search selection dropdown">
 								<input type="hidden" name="approvals_whitelist_users" value="{{.approvals_whitelist_users}}">
-								<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_users"}}</div>
+								<div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div>
 								<div class="menu">
 								{{range .Users}}
 									<div class="item" data-value="{{.ID}}">
@@ -128,7 +128,7 @@
 								<label>{{ctx.Locale.Tr "repo.settings.protect_approvals_whitelist_teams"}}</label>
 								<div class="ui multiple search selection dropdown">
 									<input type="hidden" name="approvals_whitelist_teams" value="{{.approvals_whitelist_teams}}">
-									<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_teams"}}</div>
+									<div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div>
 									<div class="menu">
 									{{range .Teams}}
 										<div class="item" data-value="{{.ID}}">
@@ -179,7 +179,7 @@
 								<tr>
 									<td>
 										<span>{{.}}</span>
-										<span class="status-check-matched-mark gt-hidden" data-status-check="{{.}}">{{ctx.Locale.Tr "repo.settings.protect_status_check_matched"}}</span>
+										<span class="status-check-matched-mark tw-hidden" data-status-check="{{.}}">{{ctx.Locale.Tr "repo.settings.protect_status_check_matched"}}</span>
 									</td>
 								</tr>
 							{{else}}
@@ -210,7 +210,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.protect_merge_whitelist_users"}}</label>
 							<div class="ui multiple search selection dropdown">
 								<input type="hidden" name="merge_whitelist_users" value="{{.merge_whitelist_users}}">
-								<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_users"}}</div>
+								<div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div>
 								<div class="menu">
 								{{range .Users}}
 									<div class="item" data-value="{{.ID}}">
@@ -225,7 +225,7 @@
 							<label>{{ctx.Locale.Tr "repo.settings.protect_merge_whitelist_teams"}}</label>
 							<div class="ui multiple search selection dropdown">
 								<input type="hidden" name="merge_whitelist_teams" value="{{.merge_whitelist_teams}}">
-								<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_teams"}}</div>
+								<div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div>
 								<div class="menu">
 								{{range .Teams}}
 									<div class="item" data-value="{{.ID}}">
diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl
index ed7762acc5..c9efb7b67e 100644
--- a/templates/repo/settings/tags.tmpl
+++ b/templates/repo/settings/tags.tmpl
@@ -1,7 +1,7 @@
 {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings edit")}}
 	<div class="repo-setting-content">
 		{{if .Repository.IsArchived}}
-			<div class="ui warning message gt-text-center">
+			<div class="ui warning message tw-text-center">
 				{{ctx.Locale.Tr "repo.settings.archive.tagsettings_unavailable"}}
 			</div>
 		{{else}}
@@ -21,14 +21,14 @@
 										<div class="ui input">
 											<input class="prompt" name="name_pattern" autocomplete="off" value="{{.name_pattern}}" placeholder="v*" autofocus required>
 										</div>
-										<div class="help">{{ctx.Locale.Tr "repo.settings.tags.protection.pattern.description" | Safe}}</div>
+										<div class="help">{{ctx.Locale.Tr "repo.settings.tags.protection.pattern.description"}}</div>
 									</div>
 								</div>
 								<div class="whitelist field">
 									<label>{{ctx.Locale.Tr "repo.settings.tags.protection.allowed.users"}}</label>
 									<div class="ui multiple search selection dropdown">
 										<input type="hidden" name="allowlist_users" value="{{.allowlist_users}}">
-										<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_users"}}</div>
+										<div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div>
 										<div class="menu">
 											{{range .Users}}
 												<div class="item" data-value="{{.ID}}">
@@ -43,7 +43,7 @@
 										<label>{{ctx.Locale.Tr "repo.settings.tags.protection.allowed.teams"}}</label>
 										<div class="ui multiple search selection dropdown">
 											<input type="hidden" name="allowlist_teams" value="{{.allowlist_teams}}">
-											<div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_teams"}}</div>
+											<div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div>
 											<div class="menu">
 												{{range .Teams}}
 													<div class="item" data-value="{{.ID}}">
@@ -106,7 +106,7 @@
 										</td>
 										<td class="right aligned">
 											<a class="ui tiny primary button" href="{{$.RepoLink}}/settings/tags/{{.ID}}">{{ctx.Locale.Tr "edit"}}</a>
-											<form class="gt-dib" action="{{$.RepoLink}}/settings/tags/delete" method="post">
+											<form class="tw-inline-block" action="{{$.RepoLink}}/settings/tags/delete" method="post">
 												{{$.CsrfTokenHtml}}
 												<input type="hidden" name="id" value="{{.ID}}">
 												<button class="ui tiny red button">{{ctx.Locale.Tr "remove"}}</button>
diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl
index ed6e670d60..36e75a7eb5 100644
--- a/templates/repo/settings/webhook/base_list.tmpl
+++ b/templates/repo/settings/webhook/base_list.tmpl
@@ -3,68 +3,23 @@
 	<div class="ui right">
 		<div class="ui jump dropdown">
 			<div class="ui primary tiny button">{{ctx.Locale.Tr "repo.settings.add_webhook"}}</div>
-			<div class="menu">
-				<a class="item" href="{{.BaseLinkNew}}/gitea/new">
-					{{template "shared/webhook/icon" (dict "HookType" "gitea" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_gitea"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/gogs/new">
-					{{template "shared/webhook/icon" (dict "HookType" "gogs" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_gogs"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/slack/new">
-					{{template "shared/webhook/icon" (dict "HookType" "slack" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_slack"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/discord/new">
-					{{template "shared/webhook/icon" (dict "HookType" "discord" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_discord"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/dingtalk/new">
-					{{template "shared/webhook/icon" (dict "HookType" "dingtalk" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/telegram/new">
-					{{template "shared/webhook/icon" (dict "HookType" "telegram" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_telegram"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/msteams/new">
-					{{template "shared/webhook/icon" (dict "HookType" "msteams" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_msteams"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/feishu/new">
-					{{template "shared/webhook/icon" (dict "HookType" "feishu" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_feishu_or_larksuite"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/matrix/new">
-					{{template "shared/webhook/icon" (dict "HookType" "matrix" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_matrix"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/wechatwork/new">
-					{{template "shared/webhook/icon" (dict "HookType" "wechatwork" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork"}}
-				</a>
-				<a class="item" href="{{.BaseLinkNew}}/packagist/new">
-					{{template "shared/webhook/icon" (dict "HookType" "packagist" "Size" 20)}}
-					{{ctx.Locale.Tr "repo.settings.web_hook_name_packagist"}}
-				</a>
-			</div>
+			{{template "repo/settings/webhook/link_menu" .}}
 		</div>
 	</div>
 </h4>
 <div class="ui attached segment">
 	<div class="ui list">
 		<div class="item">
-			{{.Description | Str2html}}
+			{{.Description}}
 		</div>
 		{{range .Webhooks}}
 			<div class="item truncated-item-container">
-				<span class="text {{if eq .LastStatus 1}}green{{else if eq .LastStatus 2}}red{{else}}grey{{end}} gt-mr-3">{{svg "octicon-dot-fill" 22}}</span>
-				<div class="text truncate gt-f1 gt-mr-3">
+				<span class="text {{if eq .LastStatus 1}}green{{else if eq .LastStatus 2}}red{{else}}grey{{end}} tw-mr-2">{{svg "octicon-dot-fill" 22}}</span>
+				<div class="text truncate tw-flex-1 tw-mr-2">
 					<a title="{{.URL}}" href="{{$.BaseLink}}/{{.ID}}">{{.URL}}</a>
 				</div>
-				<a class="muted gt-p-3" href="{{$.BaseLink}}/{{.ID}}">{{svg "octicon-pencil"}}</a>
-				<a class="delete-button gt-p-3" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}}</a>
+				<a class="muted tw-p-2" href="{{$.BaseLink}}/{{.ID}}">{{svg "octicon-pencil"}}</a>
+				<a class="delete-button tw-p-2" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}}</a>
 			</div>
 		{{end}}
 	</div>
diff --git a/templates/repo/settings/webhook/dingtalk.tmpl b/templates/repo/settings/webhook/dingtalk.tmpl
index 32ca0d0807..0ba99e98ee 100644
--- a/templates/repo/settings/webhook/dingtalk.tmpl
+++ b/templates/repo/settings/webhook/dingtalk.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "dingtalk"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://dingtalk.com" (ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://dingtalk.com" (ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/dingtalk/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
diff --git a/templates/repo/settings/webhook/discord.tmpl b/templates/repo/settings/webhook/discord.tmpl
index 25dc219ee1..104346e042 100644
--- a/templates/repo/settings/webhook/discord.tmpl
+++ b/templates/repo/settings/webhook/discord.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "discord"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://discord.com" (ctx.Locale.Tr "repo.settings.web_hook_name_discord") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://discord.com" (ctx.Locale.Tr "repo.settings.web_hook_name_discord")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/discord/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
diff --git a/templates/repo/settings/webhook/feishu.tmpl b/templates/repo/settings/webhook/feishu.tmpl
index 9683427fbf..d80deab26f 100644
--- a/templates/repo/settings/webhook/feishu.tmpl
+++ b/templates/repo/settings/webhook/feishu.tmpl
@@ -1,6 +1,6 @@
 {{if eq .HookType "feishu"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://feishu.cn" (ctx.Locale.Tr "repo.settings.web_hook_name_feishu") | Str2html}}</p>
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://larksuite.com" (ctx.Locale.Tr "repo.settings.web_hook_name_larksuite") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://feishu.cn" (ctx.Locale.Tr "repo.settings.web_hook_name_feishu")}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://larksuite.com" (ctx.Locale.Tr "repo.settings.web_hook_name_larksuite")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/feishu/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
diff --git a/templates/repo/settings/webhook/gitea.tmpl b/templates/repo/settings/webhook/gitea.tmpl
index 4fda6a7b39..e6eb61ea92 100644
--- a/templates/repo/settings/webhook/gitea.tmpl
+++ b/templates/repo/settings/webhook/gitea.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "gitea"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gitea") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gitea")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/gitea/{{or .Webhook.ID "new"}}" method="post">
 		{{template "base/disable_form_autofill"}}
 		{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/gogs.tmpl b/templates/repo/settings/webhook/gogs.tmpl
index d2bd98c32c..e91a3279e4 100644
--- a/templates/repo/settings/webhook/gogs.tmpl
+++ b/templates/repo/settings/webhook/gogs.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "gogs"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gogs") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gogs")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/gogs/{{or .Webhook.ID "new"}}" method="post">
 		{{template "base/disable_form_autofill"}}
 		{{.CsrfTokenHtml}}
diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl
index 3c21a42421..8ee1446a16 100644
--- a/templates/repo/settings/webhook/history.tmpl
+++ b/templates/repo/settings/webhook/history.tmpl
@@ -6,7 +6,9 @@
 			<div class="ui right">
 				<!-- the button is wrapped with a span because the tooltip doesn't show on hover if we put data-tooltip-content directly on the button -->
 				<span data-tooltip-content="{{if or $isNew .Webhook.IsActive}}{{ctx.Locale.Tr "repo.settings.webhook.test_delivery_desc"}}{{else}}{{ctx.Locale.Tr "repo.settings.webhook.test_delivery_desc_disabled"}}{{end}}">
-					<button class="ui teal tiny button{{if not (or $isNew .Webhook.IsActive)}} disabled{{end}}" id="test-delivery" data-link="{{.Link}}/test" data-redirect="{{.Link}}">{{ctx.Locale.Tr "repo.settings.webhook.test_delivery"}}</button>
+					<button class="ui teal tiny button{{if not (or $isNew .Webhook.IsActive)}} disabled{{end}}" id="test-delivery" data-link="{{.Link}}/test" data-redirect="{{.Link}}">
+						<span class="text">{{ctx.Locale.Tr "repo.settings.webhook.test_delivery"}}</span>
+					</button>
 			</span>
 			</div>
 		{{end}}
@@ -15,10 +17,12 @@
 		<div class="ui list">
 			{{range .History}}
 				<div class="item">
-					<div class="flex-text-block gt-sb">
+					<div class="flex-text-block tw-justify-between">
 						<div class="flex-text-inline">
 							{{if .IsSucceed}}
 								<span class="text green">{{svg "octicon-check"}}</span>
+							{{else if not .IsDelivered}}
+								<span class="text orange">{{svg "octicon-stopwatch"}}</span>
 							{{else}}
 								<span class="text red">{{svg "octicon-alert"}}</span>
 							{{end}}
@@ -28,7 +32,7 @@
 							{{TimeSince .Delivered.AsTime ctx.Locale}}
 						</span>
 					</div>
-					<div class="info gt-hidden" id="info-{{.ID}}">
+					<div class="info tw-hidden" id="info-{{.ID}}">
 						<div class="ui top attached tabular menu">
 							<a class="item active" data-tab="request-{{.ID}}">{{ctx.Locale.Tr "repo.settings.webhook.request"}}</a>
 							<a class="item" data-tab="response-{{.ID}}">
@@ -62,7 +66,7 @@
 {{range $key, $val := .RequestInfo.Headers}}<strong>{{$key}}:</strong> {{$val}}
 {{end}}</pre>
 								<h5>{{ctx.Locale.Tr "repo.settings.webhook.payload"}}</h5>
-								<pre class="webhook-info"><code class="json">{{.PayloadContent}}</code></pre>
+								<pre class="webhook-info"><code class="json">{{or .RequestInfo.Body .PayloadContent}}</code></pre>
 							{{else}}
 								-
 							{{end}}
diff --git a/templates/repo/settings/webhook/link_menu.tmpl b/templates/repo/settings/webhook/link_menu.tmpl
new file mode 100644
index 0000000000..e2c86dcc3c
--- /dev/null
+++ b/templates/repo/settings/webhook/link_menu.tmpl
@@ -0,0 +1,50 @@
+{{$size := 20}}
+{{if .Size}}
+	{{$size = .Size}}
+{{end}}
+<div class="menu">
+	<a class="item" href="{{.BaseLinkNew}}/gitea/new">
+		{{template "shared/webhook/icon" (dict "HookType" "gitea" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_gitea"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/gogs/new">
+		{{template "shared/webhook/icon" (dict "HookType" "gogs" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_gogs"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/slack/new">
+		{{template "shared/webhook/icon" (dict "HookType" "slack" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_slack"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/discord/new">
+		{{template "shared/webhook/icon" (dict "HookType" "discord" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_discord"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/dingtalk/new">
+		{{template "shared/webhook/icon" (dict "HookType" "dingtalk" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/telegram/new">
+		{{template "shared/webhook/icon" (dict "HookType" "telegram" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_telegram"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/msteams/new">
+		{{template "shared/webhook/icon" (dict "HookType" "msteams" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_msteams"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/feishu/new">
+		{{template "shared/webhook/icon" (dict "HookType" "feishu" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_feishu_or_larksuite"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/matrix/new">
+		{{template "shared/webhook/icon" (dict "HookType" "matrix" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_matrix"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/wechatwork/new">
+		{{template "shared/webhook/icon" (dict "HookType" "wechatwork" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork"}}
+	</a>
+	<a class="item" href="{{.BaseLinkNew}}/packagist/new">
+		{{template "shared/webhook/icon" (dict "HookType" "packagist" "Size" $size)}}
+		{{ctx.Locale.Tr "repo.settings.web_hook_name_packagist"}}
+	</a>
+</div>
diff --git a/templates/repo/settings/webhook/matrix.tmpl b/templates/repo/settings/webhook/matrix.tmpl
index a2a9921d7b..7f1c9f08e6 100644
--- a/templates/repo/settings/webhook/matrix.tmpl
+++ b/templates/repo/settings/webhook/matrix.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "matrix"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://matrix.org/" (ctx.Locale.Tr "repo.settings.web_hook_name_matrix") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://matrix.org/" (ctx.Locale.Tr "repo.settings.web_hook_name_matrix")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/matrix/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_HomeserverURL}}error{{end}}">
diff --git a/templates/repo/settings/webhook/msteams.tmpl b/templates/repo/settings/webhook/msteams.tmpl
index 0097209db1..62ea24e763 100644
--- a/templates/repo/settings/webhook/msteams.tmpl
+++ b/templates/repo/settings/webhook/msteams.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "msteams"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://teams.microsoft.com" (ctx.Locale.Tr "repo.settings.web_hook_name_msteams") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://teams.microsoft.com" (ctx.Locale.Tr "repo.settings.web_hook_name_msteams")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/msteams/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
diff --git a/templates/repo/settings/webhook/packagist.tmpl b/templates/repo/settings/webhook/packagist.tmpl
index fc373951d1..25aba2a435 100644
--- a/templates/repo/settings/webhook/packagist.tmpl
+++ b/templates/repo/settings/webhook/packagist.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "packagist"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://packagist.org" (ctx.Locale.Tr "repo.settings.web_hook_name_packagist") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://packagist.org" (ctx.Locale.Tr "repo.settings.web_hook_name_packagist")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/packagist/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_Username}}error{{end}}">
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index addf99d45a..6862ce5a2c 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -5,24 +5,24 @@
 		<div class="field">
 			<div class="ui radio non-events checkbox">
 				<input name="events" type="radio" value="push_only" {{if or $isNew .Webhook.PushOnly}}checked{{end}}>
-				<label>{{ctx.Locale.Tr "repo.settings.event_push_only" | Str2html}}</label>
+				<label>{{ctx.Locale.Tr "repo.settings.event_push_only"}}</label>
 			</div>
 		</div>
 		<div class="field">
 			<div class="ui radio non-events checkbox">
 				<input name="events" type="radio" value="send_everything" {{if .Webhook.SendEverything}}checked{{end}}>
-				<label>{{ctx.Locale.Tr "repo.settings.event_send_everything" | Str2html}}</label>
+				<label>{{ctx.Locale.Tr "repo.settings.event_send_everything"}}</label>
 			</div>
 		</div>
 		<div class="field">
 			<div class="ui radio events checkbox">
 				<input name="events" type="radio" value="choose_events" {{if .Webhook.ChooseEvents}}checked{{end}}>
-				<label>{{ctx.Locale.Tr "repo.settings.event_choose" | Str2html}}</label>
+				<label>{{ctx.Locale.Tr "repo.settings.event_choose"}}</label>
 			</div>
 		</div>
 	</div>
 
-	<div class="events fields ui grid {{if not .Webhook.ChooseEvents}}gt-hidden{{end}}">
+	<div class="events fields ui grid {{if not .Webhook.ChooseEvents}}tw-hidden{{end}}">
 		<!-- Repository Events -->
 		<div class="fourteen wide column">
 			<label>{{ctx.Locale.Tr "repo.settings.event_header_repository"}}</label>
@@ -254,8 +254,8 @@
 <!-- Branch filter -->
 <div class="field">
 	<label for="branch_filter">{{ctx.Locale.Tr "repo.settings.branch_filter"}}</label>
-	<input name="branch_filter" type="text" value="{{or .Webhook.BranchFilter "*"}}">
-	<span class="help">{{ctx.Locale.Tr "repo.settings.branch_filter_desc" | Str2html}}</span>
+	<input id="branch_filter" name="branch_filter" type="text" value="{{or .Webhook.BranchFilter "*"}}">
+	<span class="help">{{ctx.Locale.Tr "repo.settings.branch_filter_desc"}}</span>
 </div>
 
 <!-- Authorization Header -->
@@ -263,7 +263,7 @@
 	<label for="authorization_header">{{ctx.Locale.Tr "repo.settings.authorization_header"}}</label>
 	<input id="authorization_header" name="authorization_header" type="text" value="{{.Webhook.HeaderAuthorization}}"{{if eq .HookType "matrix"}} placeholder="Bearer $access_token" required{{end}}>
 	{{if ne .HookType "matrix"}}{{/* Matrix doesn't make the authorization optional but it is implied by the help string, should be changed.*/}}
-		<span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" "<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | Str2html}}</span>
+		<span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" ("<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | SafeHTML)}}</span>
 	{{end}}
 </div>
 
diff --git a/templates/repo/settings/webhook/slack.tmpl b/templates/repo/settings/webhook/slack.tmpl
index b367aed5ec..e7cae92d4b 100644
--- a/templates/repo/settings/webhook/slack.tmpl
+++ b/templates/repo/settings/webhook/slack.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "slack"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://slack.com" (ctx.Locale.Tr "repo.settings.web_hook_name_slack") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://slack.com" (ctx.Locale.Tr "repo.settings.web_hook_name_slack")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/slack/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
diff --git a/templates/repo/settings/webhook/telegram.tmpl b/templates/repo/settings/webhook/telegram.tmpl
index 92bbbef3fd..f92c2be0db 100644
--- a/templates/repo/settings/webhook/telegram.tmpl
+++ b/templates/repo/settings/webhook/telegram.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "telegram"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://core.telegram.org/bots" (ctx.Locale.Tr "repo.settings.web_hook_name_telegram") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://core.telegram.org/bots" (ctx.Locale.Tr "repo.settings.web_hook_name_telegram")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/telegram/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_BotToken}}error{{end}}">
diff --git a/templates/repo/settings/webhook/wechatwork.tmpl b/templates/repo/settings/webhook/wechatwork.tmpl
index 65f12998b1..78a1617123 100644
--- a/templates/repo/settings/webhook/wechatwork.tmpl
+++ b/templates/repo/settings/webhook/wechatwork.tmpl
@@ -1,5 +1,5 @@
 {{if eq .HookType "wechatwork"}}
-	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://work.weixin.qq.com" (ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork") | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://work.weixin.qq.com" (ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork")}}</p>
 	<form class="ui form" action="{{.BaseLink}}/wechatwork/{{or .Webhook.ID "new"}}" method="post">
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_PayloadURL}}error{{end}}">
diff --git a/templates/repo/star_unstar.tmpl b/templates/repo/star_unstar.tmpl
index 9c342f4065..1cdb98bf27 100644
--- a/templates/repo/star_unstar.tmpl
+++ b/templates/repo/star_unstar.tmpl
@@ -1,11 +1,10 @@
-<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}un{{end}}star">
+<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}unstar{{else}}star{{end}}">
 	<div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.star_guest_user"}}"{{end}}>
-		<button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}}>
-			{{if $.IsStaringRepo}}
-				{{svg "octicon-star-fill"}}<span class="text">{{ctx.Locale.Tr "repo.unstar"}}</span>
-			{{else}}
-				{{svg "octicon-star"}}<span class="text">{{ctx.Locale.Tr "repo.star"}}</span>
-			{{end}}
+		{{$buttonText := ctx.Locale.Tr "repo.star"}}
+		{{if $.IsStaringRepo}}{{$buttonText = ctx.Locale.Tr "repo.unstar"}}{{end}}
+		<button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}} aria-label="{{$buttonText}}">
+			{{if $.IsStaringRepo}}{{svg "octicon-star-fill"}}{{else}}{{svg "octicon-star"}}{{end}}
+			<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
 		</button>
 		<a hx-boost="false" class="ui basic label" href="{{$.RepoLink}}/stars">
 			{{CountFmt .Repository.NumStars}}
diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl
index 8edb0c1516..000e0a10c5 100644
--- a/templates/repo/sub_menu.tmpl
+++ b/templates/repo/sub_menu.tmpl
@@ -1,5 +1,5 @@
 {{if and (not .HideRepoInfo) (not .IsBlame)}}
-<div class="ui segments repository-summary gt-mt-2 gt-mb-0">
+<div class="ui segments repository-summary tw-mt-1 tw-mb-0">
 	<div class="ui segment sub-menu repository-menu">
 		{{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo)}}
 			<a class="item muted {{if .PageIsCommits}}active{{end}}" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}">
@@ -21,11 +21,11 @@
 		{{end}}
 	</div>
 	{{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo) .LanguageStats}}
-	<div class="ui segment sub-menu language-stats-details gt-hidden">
+	<div class="ui segment sub-menu language-stats-details tw-hidden">
 		{{range .LanguageStats}}
 		<div class="item">
 			<i class="color-icon" style="background-color: {{.Color}}"></i>
-			<span class="gt-font-semibold">
+			<span class="tw-font-semibold">
 				{{if eq .Language "other"}}
 					{{ctx.Locale.Tr "repo.language_other"}}
 				{{else}}
diff --git a/templates/repo/tag/list.tmpl b/templates/repo/tag/list.tmpl
index 9f0676e395..5378a8a322 100644
--- a/templates/repo/tag/list.tmpl
+++ b/templates/repo/tag/list.tmpl
@@ -5,8 +5,8 @@
 		{{template "base/alert" .}}
 		{{template "repo/release_tag_header" .}}
 		<h4 class="ui top attached header">
-			<div class="five wide column gt-df gt-ac">
-				{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.tags"}}
+			<div class="five wide column tw-flex tw-items-center">
+				{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.tags"}}
 			</div>
 		</h4>
 		{{$canReadReleases := $.Permission.CanRead $.UnitTypeReleases}}
@@ -16,38 +16,38 @@
 					{{range $idx, $release := .Releases}}
 						<tr>
 							<td class="tag">
-								<h3 class="release-tag-name gt-mb-3">
+								<h3 class="release-tag-name tw-mb-2">
 									{{if $canReadReleases}}
-										<a class="gt-df gt-ac" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
+										<a class="tw-flex tw-items-center" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
 									{{else}}
-										<a class="gt-df gt-ac" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
+										<a class="tw-flex tw-items-center" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
 									{{end}}
 								</h3>
-								<div class="download gt-df gt-ac">
+								<div class="download tw-flex tw-items-center">
 									{{if $.Permission.CanRead $.UnitTypeCode}}
 										{{if .CreatedUnix}}
-											<span class="gt-mr-3">{{svg "octicon-clock" 16 "gt-mr-2"}}{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span>
+											<span class="tw-mr-2">{{svg "octicon-clock" 16 "tw-mr-1"}}{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span>
 										{{end}}
 
-										<a class="gt-mr-3 gt-mono muted" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "gt-mr-2"}}{{ShortSha .Sha1}}</a>
+										<a class="tw-mr-2 tw-font-mono muted" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .Sha1}}</a>
 
 										{{if not $.DisableDownloadSourceArchives}}
-											<a class="archive-link gt-mr-3 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-2"}}ZIP</a>
-											<a class="archive-link gt-mr-3 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-2"}}TAR.GZ</a>
+											<a class="archive-link tw-mr-2 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-1"}}ZIP</a>
+											<a class="archive-link tw-mr-2 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-1"}}TAR.GZ</a>
 										{{end}}
 
 										{{if (and $canReadReleases $.CanCreateRelease $release.IsTag)}}
-											<a class="gt-mr-3 muted" href="{{$.RepoLink}}/releases/new?tag={{.TagName}}">{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.new_release"}}</a>
+											<a class="tw-mr-2 muted" href="{{$.RepoLink}}/releases/new?tag={{.TagName}}">{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.new_release"}}</a>
 										{{end}}
 
 										{{if (and ($.Permission.CanWrite $.UnitTypeCode) $release.IsTag)}}
-											<a class="ui delete-button gt-mr-3 muted" data-url="{{$.RepoLink}}/tags/delete" data-id="{{.ID}}">
-												{{svg "octicon-trash" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.delete_tag"}}
+											<a class="ui delete-button tw-mr-2 muted" data-url="{{$.RepoLink}}/tags/delete" data-id="{{.ID}}">
+												{{svg "octicon-trash" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.delete_tag"}}
 											</a>
 										{{end}}
 
 										{{if and $canReadReleases (not $release.IsTag)}}
-											<a class="gt-mr-3 muted" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.detail"}}</a>
+											<a class="tw-mr-2 muted" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.detail"}}</a>
 										{{end}}
 									{{end}}
 								</div>
diff --git a/templates/repo/unicode_escape_prompt.tmpl b/templates/repo/unicode_escape_prompt.tmpl
index d0730f23c1..8bceafa8bb 100644
--- a/templates/repo/unicode_escape_prompt.tmpl
+++ b/templates/repo/unicode_escape_prompt.tmpl
@@ -1,22 +1,22 @@
 {{if .EscapeStatus}}
 	{{if .EscapeStatus.HasInvisible}}
-		<div class="ui warning message unicode-escape-prompt gt-text-left">
+		<div class="ui warning message unicode-escape-prompt tw-text-left">
 			<button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button>
 			<div class="header">
 				{{ctx.Locale.Tr "repo.invisible_runes_header"}}
 			</div>
-			<p>{{ctx.Locale.Tr "repo.invisible_runes_description" | Str2html}}</p>
+			<p>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</p>
 			{{if .EscapeStatus.HasAmbiguous}}
-				<p>{{ctx.Locale.Tr "repo.ambiguous_runes_description" | Str2html}}</p>
+				<p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p>
 			{{end}}
 		</div>
 	{{else if .EscapeStatus.HasAmbiguous}}
-		<div class="ui warning message unicode-escape-prompt gt-text-left">
+		<div class="ui warning message unicode-escape-prompt tw-text-left">
 			<button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button>
 			<div class="header">
 				{{ctx.Locale.Tr "repo.ambiguous_runes_header"}}
 			</div>
-			<p>{{ctx.Locale.Tr "repo.ambiguous_runes_description" | Str2html}}</p>
+			<p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p>
 		</div>
 	{{end}}
 {{end}}
diff --git a/templates/repo/user_cards.tmpl b/templates/repo/user_cards.tmpl
index 12fb23f067..5accc2c7af 100644
--- a/templates/repo/user_cards.tmpl
+++ b/templates/repo/user_cards.tmpl
@@ -18,7 +18,7 @@
 					{{else if .Location}}
 						{{svg "octicon-location"}} {{.Location}}
 					{{else}}
-						{{svg "octicon-calendar"}} {{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix) | Safe}}
+						{{svg "octicon-calendar"}} {{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix)}}
 					{{end}}
 				</div>
 			</li>
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index 1f9e0b5028..0683004718 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -1,23 +1,23 @@
 <div {{if .ReadmeInList}}id="readme" {{end}}class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content">
 	{{- if .FileError}}
 		<div class="ui error message">
-			<div class="text left gt-whitespace-pre">{{.FileError}}</div>
+			<div class="text left tw-whitespace-pre">{{.FileError}}</div>
 		</div>
 	{{end}}
 	{{- if .FileWarning}}
 		<div class="ui warning message">
-			<div class="text left gt-whitespace-pre">{{.FileWarning}}</div>
+			<div class="text left tw-whitespace-pre">{{.FileWarning}}</div>
 		</div>
 	{{end}}
 
 	{{if not .ReadmeInList}}
-		<div id="repo-file-commit-box" class="ui top attached header list-header gt-mb-4">
-			<div>
+		<div id="repo-file-commit-box" class="ui top attached header list-header tw-mb-4 tw-flex tw-justify-between">
+			<div class="latest-commit">
 				{{template "repo/latest_commit" .}}
 			</div>
 			{{if .LatestCommit}}
 				{{if .LatestCommit.Committer}}
-					<div class="ui text grey right age">
+					<div class="text grey age">
 						{{TimeSince .LatestCommit.Committer.When ctx.Locale}}
 					</div>
 				{{end}}
@@ -25,24 +25,24 @@
 		</div>
 	{{end}}
 
-	<h4 class="file-header ui top attached header gt-df gt-ac gt-sb gt-fw">
-		<div class="file-header-left gt-df gt-ac gt-py-3 gt-pr-4">
+	<h4 class="file-header ui top attached header tw-flex tw-items-center tw-justify-between tw-flex-wrap">
+		<div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4">
 			{{if .ReadmeInList}}
-				{{svg "octicon-book" 16 "gt-mr-3"}}
+				{{svg "octicon-book" 16 "tw-mr-2"}}
 				<strong><a class="default-link muted" href="#readme">{{.FileName}}</a></strong>
 			{{else}}
 				{{template "repo/file_info" .}}
 			{{end}}
 		</div>
-		<div class="file-header-right file-actions gt-df gt-ac gt-fw">
+		<div class="file-header-right file-actions tw-flex tw-items-center tw-flex-wrap">
 			{{if .HasSourceRenderedToggle}}
 				<div class="ui compact icon buttons">
-					<a href="{{$.Link}}?display=source" class="ui mini basic button {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a>
+					<a href="?display=source" class="ui mini basic button {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a>
 					<a href="{{$.Link}}" class="ui mini basic button {{if .IsDisplayingRendered}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_rendered"}}">{{svg "octicon-file" 15}}</a>
 				</div>
 			{{end}}
 			{{if not .ReadmeInList}}
-				<div class="ui buttons gt-mr-2">
+				<div class="ui buttons tw-mr-1">
 					<a class="ui mini basic button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
 					{{if not .IsViewCommit}}
 						<a class="ui mini basic button" href="{{.RepoLink}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a>
@@ -52,7 +52,7 @@
 					{{end}}
 					<a class="ui mini basic button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_history"}}</a>
 					{{if .EscapeStatus.Escaped}}
-						<button class="ui mini basic button unescape-button gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
+						<button class="ui mini basic button unescape-button tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
 						<button class="ui mini basic button escape-button">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
 					{{end}}
 				</div>
@@ -76,8 +76,8 @@
 					{{end}}
 				{{end}}
 			{{else if .EscapeStatus.Escaped}}
-				<button class="ui mini basic button unescape-button gt-mr-2 gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
-				<button class="ui mini basic button escape-button gt-mr-2">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
+				<button class="ui mini basic button unescape-button tw-mr-1 tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
+				<button class="ui mini basic button escape-button tw-mr-1">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
 			{{end}}
 			{{if and .ReadmeInList .CanEditReadmeFile}}
 				<a class="btn-octicon" data-tooltip-content="{{ctx.Locale.Tr "repo.editor.edit_this_file"}}" href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}/{{PathEscapeSegments .FileName}}">{{svg "octicon-pencil"}}</a>
@@ -89,7 +89,9 @@
 			{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
 		{{end}}
 		<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextSource}} code-view{{end}}">
-			{{if .IsMarkup}}
+			{{if .IsFileTooLarge}}
+				{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
+			{{else if .IsMarkup}}
 				{{if .FileContent}}{{.FileContent}}{{end}}
 			{{else if .IsPlainText}}
 				<pre>{{if .FileContent}}{{.FileContent}}{{end}}</pre>
@@ -108,19 +110,10 @@
 					{{else if .IsPDFFile}}
 						<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
 					{{else}}
-						<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
+						<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
 					{{end}}
 				</div>
 			{{else if .FileSize}}
-				{{if .IsFileTooLarge}}
-				<table>
-					<tbody>
-						<tr>
-							<td><strong>{{ctx.Locale.Tr "repo.file_too_large"}}</strong></td>
-						</tr>
-					</tbody>
-				</table>
-				{{else}}
 				<table>
 					<tbody>
 						{{range $idx, $code := .FileContent}}
@@ -135,14 +128,13 @@
 						{{end}}
 					</tbody>
 				</table>
-				<div class="code-line-menu ui vertical pointing menu tippy-target">
+				<div class="code-line-menu tippy-target">
 					{{if $.Permission.CanRead $.UnitTypeIssues}}
-						<a class="item ref-in-new-issue" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a>
+						<a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a>
 					{{end}}
-					<a class="item view_git_blame" href="{{.Repository.Link}}/blame/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.view_git_blame"}}</a>
-					<a class="item copy-line-permalink" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a>
+					<a class="item view_git_blame" role="menuitem" href="{{.Repository.Link}}/blame/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.view_git_blame"}}</a>
+					<a class="item copy-line-permalink" role="menuitem" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a>
 				</div>
-				{{end}}
 			{{end}}
 		</div>
 	</div>
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl
index c1ef4ff4cb..fb257bd474 100644
--- a/templates/repo/view_list.tmpl
+++ b/templates/repo/view_list.tmpl
@@ -1,8 +1,12 @@
-<table id="repo-files-table" class="ui single line table gt-mt-0" data-last-commit-loader-url="{{.LastCommitLoaderURL}}">
+<table id="repo-files-table" class="ui single line table tw-mt-0" {{if .HasFilesWithoutLatestCommit}}hx-indicator="tr.notready td.message span" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
 	<thead>
 		<tr class="commit-list">
-			<th colspan="2" {{if not .LatestCommit}}class="notready"{{end}}>
-				{{template "repo/latest_commit" .}}
+			<th class="tw-overflow-hidden" colspan="2">
+				<div class="tw-flex">
+					<div class="latest-commit">
+						{{template "repo/latest_commit" .}}
+					</div>
+				</div>
 			</th>
 			<th class="text grey right age">{{if .LatestCommit}}{{if .LatestCommit.Committer}}{{TimeSince .LatestCommit.Committer.When ctx.Locale}}{{end}}{{end}}</th>
 		</tr>
@@ -55,7 +59,7 @@
 							{{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}}
 							{{RenderCommitMessageLinkSubject $.Context $commit.Message $commitLink ($.Repository.ComposeMetas ctx)}}
 						{{else}}
-							<div class="ui active tiny slow centered inline">…</div>
+							<div class="ui active tiny slow centered inline"></div>
 						{{end}}
 					</span>
 				</td>
diff --git a/templates/repo/watch_unwatch.tmpl b/templates/repo/watch_unwatch.tmpl
index c42bc5a9e7..64be971416 100644
--- a/templates/repo/watch_unwatch.tmpl
+++ b/templates/repo/watch_unwatch.tmpl
@@ -1,11 +1,10 @@
-<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}un{{end}}watch">
+<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}unwatch{{else}}watch{{end}}">
 	<div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.watch_guest_user"}}"{{end}}>
-		<button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}}>
-			{{if $.IsWatchingRepo}}
-				{{svg "octicon-eye-closed" 16}}<span class="text">{{ctx.Locale.Tr "repo.unwatch"}}</span>
-			{{else}}
-				{{svg "octicon-eye"}}<span class="text">{{ctx.Locale.Tr "repo.watch"}}</span>
-			{{end}}
+		{{$buttonText := ctx.Locale.Tr "repo.watch"}}
+		{{if $.IsWatchingRepo}}{{$buttonText = ctx.Locale.Tr "repo.unwatch"}}{{end}}
+		<button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}} aria-label="{{$buttonText}}">
+			{{svg "octicon-eye"}}
+			<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
 		</button>
 		<a hx-boost="false" class="ui basic label" href="{{.RepoLink}}/watchers">
 			{{CountFmt .Repository.NumWatches}}
diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl
index ff31df0c32..0f10e60c4f 100644
--- a/templates/repo/wiki/new.tmpl
+++ b/templates/repo/wiki/new.tmpl
@@ -3,13 +3,13 @@
 	{{template "repo/header" .}}
 	<div class="ui container">
 		{{template "base/alert" .}}
-		<div class="ui header flex-text-block gt-sb">
+		<div class="ui header flex-text-block tw-justify-between">
 			{{ctx.Locale.Tr "repo.wiki.new_page"}}
 			{{if .PageIsWikiEdit}}
 				<a class="ui tiny primary button" href="{{.RepoLink}}/wiki?action=_new">{{ctx.Locale.Tr "repo.wiki.new_page_button"}}</a>
 			{{end}}
 		</div>
-		<form class="ui form" action="{{.Link}}?action={{if .PageIsWikiEdit}}_edit{{else}}_new{{end}}" method="post">
+		<form class="ui form" action="?action={{if .PageIsWikiEdit}}_edit{{else}}_new{{end}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="field {{if .Err_Title}}error{{end}}">
 				<input name="title" value="{{.title}}" aria-label="{{ctx.Locale.Tr "repo.wiki.page_title"}}" placeholder="{{ctx.Locale.Tr "repo.wiki.page_title"}}" autofocus required>
@@ -31,14 +31,13 @@
 				"TextareaContent" $content
 			)}}
 
-			<div class="field gt-mt-4">
+			<div class="field tw-mt-4">
 				<input name="message" aria-label="{{ctx.Locale.Tr "repo.wiki.default_commit_message"}}" placeholder="{{ctx.Locale.Tr "repo.wiki.default_commit_message"}}">
 			</div>
 			<div class="divider"></div>
 			<div class="text right">
-				<button class="ui primary button">
-					{{ctx.Locale.Tr "repo.wiki.save_page"}}
-				</button>
+				<a class="ui basic cancel button" href="{{.Link}}">{{ctx.Locale.Tr "cancel"}}</a>
+				<button class="ui primary button">{{ctx.Locale.Tr "repo.wiki.save_page"}}</button>
 			</div>
 		</form>
 	</div>
diff --git a/templates/repo/wiki/pages.tmpl b/templates/repo/wiki/pages.tmpl
index a1bf13287c..52bf165e38 100644
--- a/templates/repo/wiki/pages.tmpl
+++ b/templates/repo/wiki/pages.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository wiki pages">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<h2 class="ui header gt-df gt-ac gt-sb">
+		<h2 class="ui header tw-flex tw-items-center tw-justify-between">
 			<span>{{ctx.Locale.Tr "repo.wiki.pages"}}</span>
 			<span>
 				{{if and .CanWriteWiki (not .Repository.IsMirror)}}
@@ -10,6 +10,7 @@
 				{{end}}
 			</span>
 		</h2>
+		{{if .IsRepositoryAdmin}}<div>{{ctx.Locale.Tr "repo.default_branch"}}: {{.Repository.DefaultWikiBranch}}</div>{{end}}
 		<table class="ui table wiki-pages-list">
 			<tbody>
 				{{range .Pages}}
@@ -20,7 +21,7 @@
 							<a class="wiki-git-entry" href="{{$.RepoLink}}/wiki/{{.GitEntryName | PathEscape}}" data-tooltip-content="{{ctx.Locale.Tr "repo.wiki.original_git_entry_tooltip"}}">{{svg "octicon-chevron-right"}}</a>
 						</td>
 						{{$timeSince := TimeSinceUnix .UpdatedUnix ctx.Locale}}
-						<td class="text right">{{ctx.Locale.Tr "repo.wiki.last_updated" $timeSince | Safe}}</td>
+						<td class="text right">{{ctx.Locale.Tr "repo.wiki.last_updated" $timeSince}}</td>
 					</tr>
 				{{end}}
 			</tbody>
diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl
index 95b3cd0920..8e0060d4b3 100644
--- a/templates/repo/wiki/revision.tmpl
+++ b/templates/repo/wiki/revision.tmpl
@@ -10,19 +10,19 @@
 					{{$title}}
 					<div class="ui sub header gt-word-break">
 						{{$timeSince := TimeSince .Author.When ctx.Locale}}
-						{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince | Safe}}
+						{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}}
 					</div>
 				</div>
 			</div>
 			<div class="ui eight wide column text right">
-				<div class="ui action small input" id="clone-panel">
+				<div class="clone-panel ui action small input">
 					{{template "repo/clone_buttons" .}}
 					{{template "repo/clone_script" .}}
 				</div>
 			</div>
 		</div>
 		<h2 class="ui top header">{{ctx.Locale.Tr "repo.wiki.wiki_page_revisions"}}</h2>
-		<div class="gt-mt-4">
+		<div class="tw-mt-4">
 			<h4 class="ui top attached header">
 				<div class="ui stackable grid">
 					<div class="sixteen wide column">
diff --git a/templates/repo/wiki/start.tmpl b/templates/repo/wiki/start.tmpl
index dca7a074aa..1b3c3d538a 100644
--- a/templates/repo/wiki/start.tmpl
+++ b/templates/repo/wiki/start.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository wiki start">
 	{{template "repo/header" .}}
 	<div class="ui container">
-		<div class="ui center segment gt-py-5">
+		<div class="ui center segment tw-py-8">
 			{{svg "octicon-book" 48}}
 			<h2>{{ctx.Locale.Tr "repo.wiki.welcome"}}</h2>
 			<p>{{ctx.Locale.Tr "repo.wiki.welcome_desc"}}</p>
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index 039ff3f179..409de3ff08 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -4,8 +4,8 @@
 	{{$title := .title}}
 	<div class="ui container">
 		<div class="repo-button-row">
-			<div class="gt-df gt-ac">
-				<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "repo.pulls.no_results"}}">
+			<div class="tw-flex tw-items-center">
+				<div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}">
 					<div class="ui basic small button">
 						<span class="text">
 							{{ctx.Locale.Tr "repo.wiki.page"}}:
@@ -28,7 +28,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="ui action small input gt-df gt-ac" id="clone-panel">
+			<div class="clone-panel ui action small input">
 				{{template "repo/clone_buttons" .}}
 				{{template "repo/clone_script" .}}
 			</div>
@@ -40,12 +40,12 @@
 					{{$title}}
 					<div class="ui sub header">
 						{{$timeSince := TimeSince .Author.When ctx.Locale}}
-						{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince | Safe}}
+						{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}}
 					</div>
 				</div>
 				<div class="eight wide right aligned column">
 					{{if .EscapeStatus.Escaped}}
-						<a class="ui small button unescape-button gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
+						<a class="ui small button unescape-button tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
 						<a class="ui small button escape-button">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a>
 					{{end}}
 					{{if and .CanWriteWiki (not .Repository.IsMirror)}}
@@ -67,34 +67,34 @@
 		<div class="wiki-content-parts">
 			{{if .sidebarTocContent}}
 			<div class="markup wiki-content-sidebar wiki-content-toc">
-				{{.sidebarTocContent | Safe}}
+				{{.sidebarTocContent | SafeHTML}}
 			</div>
 			{{end}}
 
 			<div class="markup wiki-content-main {{if or .sidebarTocContent .sidebarPresent}}with-sidebar{{end}}">
 				{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
-				{{.content | Safe}}
+				{{.content | SafeHTML}}
 			</div>
 
 			{{if .sidebarPresent}}
 			<div class="markup wiki-content-sidebar">
 				{{if and .CanWriteWiki (not .Repository.IsMirror)}}
-					<a class="gt-float-right muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
+					<a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
 				{{end}}
 				{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}}
-				{{.sidebarContent | Safe}}
+				{{.sidebarContent | SafeHTML}}
 			</div>
 			{{end}}
 
-			<div class="gt-clear-both"></div>
+			<div class="tw-clear-both"></div>
 
 			{{if .footerPresent}}
 			<div class="markup wiki-content-footer">
 				{{if and .CanWriteWiki (not .Repository.IsMirror)}}
-					<a class="gt-float-right muted" href="{{.RepoLink}}/wiki/_Footer?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
+					<a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Footer?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
 				{{end}}
 				{{template "repo/unicode_escape_prompt" dict "footerEscapeStatus" .sidebarEscapeStatus "root" $}}
-				{{.footerContent | Safe}}
+				{{.footerContent | SafeHTML}}
 			</div>
 			{{end}}
 		</div>
@@ -107,7 +107,7 @@
 		{{ctx.Locale.Tr "repo.wiki.delete_page_button"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "repo.wiki.delete_page_notice_1" ($title|Escape) | Safe}}</p>
+		<p>{{ctx.Locale.Tr "repo.wiki.delete_page_notice_1" $title}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/shared/actions/runner_badge.tmpl b/templates/shared/actions/runner_badge.tmpl
new file mode 100644
index 0000000000..816e87e177
--- /dev/null
+++ b/templates/shared/actions/runner_badge.tmpl
@@ -0,0 +1,25 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Badge.Width}}" height="18"
+	role="img" aria-label="{{.Badge.Label.Text}}: {{.Badge.Message.Text}}">
+	<title>{{.Badge.Label.Text}}: {{.Badge.Message.Text}}</title>
+	<linearGradient id="s" x2="0" y2="100%">
+		<stop offset="0" stop-color="#fff" stop-opacity=".7" />
+		<stop offset=".1" stop-color="#aaa" stop-opacity=".1" />
+		<stop offset=".9" stop-color="#000" stop-opacity=".3" />
+		<stop offset="1" stop-color="#000" stop-opacity=".5" />
+	</linearGradient>
+	<clipPath id="r">
+		<rect width="{{.Badge.Width}}" height="18" rx="4" fill="#fff" />
+	</clipPath>
+	<g clip-path="url(#r)">
+		<rect width="{{.Badge.Label.Width}}" height="18" fill="#555" />
+		<rect x="{{.Badge.Label.Width}}" width="{{.Badge.Message.Width}}" height="18" fill="{{.Badge.Color}}" />
+		<rect width="{{.Badge.Width}}" height="18" fill="url(#s)" />
+	</g>
+	<g fill="#fff" text-anchor="middle" font-family="Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision"
+		font-size="{{.Badge.FontSize}}"><text aria-hidden="true" x="{{.Badge.Label.X}}" y="140" fill="#010101" fill-opacity=".3"
+			transform="scale(.1)" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text x="{{.Badge.Label.X}}" y="130"
+			transform="scale(.1)" fill="#fff" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text aria-hidden="true"
+			x="{{.Badge.Message.X}}" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)"
+			textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text><text x="{{.Badge.Message.X}}" y="130" transform="scale(.1)"
+			fill="#fff" textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text></g>
+</svg>
diff --git a/templates/shared/actions/runner_edit.tmpl b/templates/shared/actions/runner_edit.tmpl
index c10901501d..d60f10b71f 100644
--- a/templates/shared/actions/runner_edit.tmpl
+++ b/templates/shared/actions/runner_edit.tmpl
@@ -7,15 +7,15 @@
 			{{template "base/disable_form_autofill"}}
 			{{.CsrfTokenHtml}}
 			<div class="runner-basic-info">
-				<div class="field gt-dib gt-mr-4">
+				<div class="field tw-inline-block tw-mr-4">
 					<label>{{ctx.Locale.Tr "actions.runners.status"}}</label>
 					<span class="ui {{if .Runner.IsOnline}}green{{else}}basic{{end}} label">{{.Runner.StatusLocaleName ctx.Locale}}</span>
 				</div>
-				<div class="field gt-dib gt-mr-4">
+				<div class="field tw-inline-block tw-mr-4">
 					<label>{{ctx.Locale.Tr "actions.runners.last_online"}}</label>
 					<span>{{if .Runner.LastOnline}}{{TimeSinceUnix .Runner.LastOnline ctx.Locale}}{{else}}{{ctx.Locale.Tr "never"}}{{end}}</span>
 				</div>
-				<div class="field gt-dib gt-mr-4">
+				<div class="field tw-inline-block tw-mr-4">
 					<label>{{ctx.Locale.Tr "actions.runners.labels"}}</label>
 					<span>
 						{{range .Runner.AgentLabels}}
@@ -23,7 +23,7 @@
 						{{end}}
 					</span>
 				</div>
-				<div class="field gt-dib gt-mr-4">
+				<div class="field tw-inline-block tw-mr-4">
 					<label>{{ctx.Locale.Tr "actions.runners.owner_type"}}</label>
 					<span data-tooltip-content="{{.Runner.BelongsToOwnerName}}">{{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}</span>
 				</div>
@@ -89,7 +89,7 @@
 			{{ctx.Locale.Tr "actions.runners.delete_runner_header"}}
 		</div>
 		<div class="content">
-			<p>{{ctx.Locale.Tr "actions.runners.delete_runner_notice" | Safe}}</p>
+			<p>{{ctx.Locale.Tr "actions.runners.delete_runner_notice"}}</p>
 		</div>
 		{{template "base/modal_actions_confirm" .}}
 	</div>
diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl
index 0e8f3cb874..8163007993 100644
--- a/templates/shared/actions/runner_list.tmpl
+++ b/templates/shared/actions/runner_list.tmpl
@@ -33,11 +33,7 @@
 	</h4>
 	<div class="ui attached segment">
 		<form class="ui form ignore-dirty" id="user-list-search-form" action="{{$.Link}}">
-			<!-- Search Text -->
-			<div class="ui fluid action input">
-				{{template "shared/searchinput" dict "Value" .Keyword}}
-				<button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button>
-			</div>
+			{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.runner_kind")}}
 		</form>
 	</div>
 	<div class="ui attached table segment">
@@ -68,7 +64,7 @@
 					{{range .Runners}}
 					<tr>
 						<td>
-							<span class="ui {{if .IsOnline}}green{{else}}basic{{end}} label">{{.StatusLocaleName ctx.Locale}}</span>
+							<span class="ui {{if .IsOnline}}green{{end}} label">{{.StatusLocaleName ctx.Locale}}</span>
 						</td>
 						<td>{{.ID}}</td>
 						<td><p data-tooltip-content="{{.Description}}">{{.Name}}</p></td>
diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl
index c6e86133cd..96fcf04cef 100644
--- a/templates/shared/combomarkdowneditor.tmpl
+++ b/templates/shared/combomarkdowneditor.tmpl
@@ -49,7 +49,7 @@ Template Attributes:
 		</text-expander>
 		<script>
 			if (localStorage?.getItem('markdown-editor-monospace') === 'true') {
-				document.querySelector('.markdown-text-editor').classList.add('gt-mono');
+				document.querySelector('.markdown-text-editor').classList.add('tw-font-mono');
 			}
 		</script>
 	</div>
diff --git a/templates/shared/filetoolarge.tmpl b/templates/shared/filetoolarge.tmpl
new file mode 100644
index 0000000000..8842fb1b91
--- /dev/null
+++ b/templates/shared/filetoolarge.tmpl
@@ -0,0 +1,4 @@
+<div class="tw-p-4">
+	{{ctx.Locale.Tr "repo.file_too_large"}}
+	{{if .RawFileLink}}<a href="{{.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>{{end}}
+</div>
diff --git a/templates/shared/issueicon.tmpl b/templates/shared/issueicon.tmpl
index 089e80bd8b..a62714e988 100644
--- a/templates/shared/issueicon.tmpl
+++ b/templates/shared/issueicon.tmpl
@@ -1,15 +1,15 @@
 {{if .IsPull}}
-	{{if and .PullRequest .PullRequest.HasMerged}}
-		{{svg "octicon-git-merge" 16 "text purple"}}
-	{{else if and (.GetPullRequest ctx) (.GetPullRequest ctx).HasMerged}}
-		{{svg "octicon-git-merge" 16 "text purple"}}
+	{{if not .PullRequest}}
+		No PullRequest
 	{{else}}
 		{{if .IsClosed}}
-			{{svg "octicon-git-pull-request" 16 "text red"}}
+			{{if .PullRequest.HasMerged}}
+				{{svg "octicon-git-merge" 16 "text purple"}}
+			{{else}}
+				{{svg "octicon-git-pull-request" 16 "text red"}}
+			{{end}}
 		{{else}}
-			{{if and .PullRequest (.PullRequest.IsWorkInProgress ctx)}}
-				{{svg "octicon-git-pull-request-draft" 16 "text grey"}}
-			{{else if and (.GetPullRequest ctx) ((.GetPullRequest ctx).IsWorkInProgress ctx)}}
+			{{if .PullRequest.IsWorkInProgress ctx}}
 				{{svg "octicon-git-pull-request-draft" 16 "text grey"}}
 			{{else}}
 				{{svg "octicon-git-pull-request" 16 "text green"}}
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl
index 8fe5aadf2b..1c0dfcc551 100644
--- a/templates/shared/issuelist.tmpl
+++ b/templates/shared/issuelist.tmpl
@@ -5,7 +5,7 @@
 
 			<div class="flex-item-icon">
 				{{if $.CanWriteIssuesOrPulls}}
-				<input type="checkbox" autocomplete="off" class="issue-checkbox gt-mr-4" data-issue-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} &quot;{{.Title}}&quot;">
+				<input type="checkbox" autocomplete="off" class="issue-checkbox tw-mr-4" data-issue-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} &quot;{{.Title}}&quot;">
 				{{end}}
 				{{template "shared/issueicon" .}}
 			</div>
@@ -13,15 +13,15 @@
 			<div class="flex-item-main">
 				<div class="flex-item-header">
 					<div class="flex-item-title">
-						<a class="gt-no-underline issue-title" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">{{RenderEmoji $.Context .Title | RenderCodeBlock}}</a>
+						<a class="tw-no-underline issue-title" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">{{RenderEmoji $.Context .Title | RenderCodeBlock}}</a>
 						{{if .IsPull}}
 							{{if (index $.CommitStatuses .PullRequest.ID)}}
 								{{template "repo/commit_statuses" dict "Status" (index $.CommitLastStatus .PullRequest.ID) "Statuses" (index $.CommitStatuses .PullRequest.ID)}}
 							{{end}}
 						{{end}}
-						<span class="labels-list gt-ml-2">
+						<span class="labels-list tw-ml-1">
 							{{range .Labels}}
-								<a href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{RenderLabel $.Context .}}</a>
+								<a href="?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{RenderLabel $.Context ctx.Locale .}}</a>
 							{{end}}
 						</span>
 					</div>
@@ -36,7 +36,7 @@
 						{{if .Assignees}}
 						<div class="text grey">
 							{{range .Assignees}}
-								<a class="ui assignee gt-no-underline" href="{{.HomeLink}}" data-tooltip-content="{{.GetDisplayName}}">
+								<a class="ui assignee tw-no-underline" href="{{.HomeLink}}" data-tooltip-content="{{.GetDisplayName}}">
 									{{ctx.AvatarUtils.Avatar . 20}}
 								</a>
 							{{end}}
@@ -44,7 +44,7 @@
 						{{end}}
 						{{if .NumComments}}
 						<div class="text grey">
-							<a class="gt-no-underline muted flex-text-block" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
+							<a class="tw-no-underline muted flex-text-block" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
 								{{svg "octicon-comment" 16}}{{.NumComments}}
 							</a>
 						</div>
@@ -62,11 +62,11 @@
 					</a>
 					{{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}}
 					{{if .OriginalAuthor}}
-						{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}}
+						{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .OriginalAuthor}}
 					{{else if gt .Poster.ID 0}}
-						{{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape) | Safe}}
+						{{ctx.Locale.Tr .GetLastEventLabel $timeStr .Poster.HomeLink .Poster.GetDisplayName}}
 					{{else}}
-						{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape) | Safe}}
+						{{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .Poster.GetDisplayName}}
 					{{end}}
 					{{if .IsPull}}
 						<div class="branches flex-text-inline">
@@ -153,7 +153,7 @@
 	{{end}}
 	{{if .IssueIndexerUnavailable}}
 		<div class="ui error message">
-			<p>{{ctx.Locale.Tr "repo.issues.keyword_search_unavailable"}}</p>
+			<p>{{ctx.Locale.Tr "search.keyword_search_unavailable"}}</p>
 		</div>
 	{{end}}
 </div>
diff --git a/templates/shared/repo_search.tmpl b/templates/shared/repo_search.tmpl
new file mode 100644
index 0000000000..7fcb5d2361
--- /dev/null
+++ b/templates/shared/repo_search.tmpl
@@ -0,0 +1,64 @@
+<div class="ui small secondary filter menu">
+	<form id="repo-search-form" class="ui form ignore-dirty tw-flex-1 tw-flex tw-gap-x-2">
+		{{if .Language}}<input type="hidden" name="language" value="{{.Language}}">{{end}}
+		{{if .PageIsExploreRepositories}}<input type="hidden" name="only_show_relevant" value="{{.OnlyShowRelevant}}">{{end}}
+		{{if .TabName}}<input type="hidden" name="tab" value="{{.TabName}}">{{end}}
+		{{if .TopicOnly}}<input type="hidden" name="topic" value="{{.TopicOnly}}">{{end}}
+		<div class="ui small fluid action input tw-flex-1">
+			{{template "shared/search/input" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.repo_kind")}}
+			{{template "shared/search/button"}}
+		</div>
+		<!-- Filter -->
+		<div class="item ui small dropdown jump">
+			<span class="text">{{ctx.Locale.Tr "filter"}}</span>
+			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+			<div class="menu flex-items-menu">
+				<label class="item"><input type="radio" name="clear-filter"> {{ctx.Locale.Tr "filter.clear"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="archived" {{if .IsArchived.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_archived"}}</label>
+				<label class="item"><input type="radio" name="archived" {{if (not (.IsArchived.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_archived"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="fork" {{if .IsFork.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_fork"}}</label>
+				<label class="item"><input type="radio" name="fork" {{if (not (.IsFork.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_fork"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="mirror" {{if .IsMirror.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_mirror"}}</label>
+				<label class="item"><input type="radio" name="mirror" {{if (not (.IsMirror.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_mirror"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="template" {{if .IsTemplate.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_template"}}</label>
+				<label class="item"><input type="radio" name="template" {{if (not (.IsTemplate.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_template"}}</label>
+				<div class="divider"></div>
+				<label class="item"><input type="radio" name="private" {{if .IsPrivate.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.private"}}</label>
+				<label class="item"><input type="radio" name="private" {{if (not (.IsPrivate.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.public"}}</label>
+			</div>
+		</div>
+		<!-- Sort -->
+		<div class="item ui small dropdown jump">
+			<span class="text">{{ctx.Locale.Tr "repo.issues.filter_sort"}}</span>
+			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+			<div class="menu">
+				<label class="{{if eq .SortType "newest"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "newest"}}checked{{end}} value="newest"> {{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</label>
+				<label class="{{if eq .SortType "oldest"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "oldest"}}checked{{end}} value="oldest"> {{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</label>
+				<label class="{{if eq .SortType "alphabetically"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "alphabetically"}}checked{{end}} value="alphabetically"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</label>
+				<label class="{{if eq .SortType "reversealphabetically"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "reversealphabetically"}}checked{{end}} value="reversealphabetically"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</label>
+				<label class="{{if eq .SortType "recentupdate"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "recentupdate"}}checked{{end}} value="recentupdate"> {{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</label>
+				<label class="{{if eq .SortType "leastupdate"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "leastupdate"}}checked{{end}} value="leastupdate"> {{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</label>
+				{{if not .DisableStars}}
+					<label class="{{if eq .SortType "moststars"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "moststars"}}checked{{end}} value="moststars"> {{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</label>
+					<label class="{{if eq .SortType "feweststars"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "feweststars"}}checked{{end}} value="feweststars"> {{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</label>
+				{{end}}
+				<label class="{{if eq .SortType "mostforks"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "mostforks"}}checked{{end}} value="mostforks"> {{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</label>
+				<label class="{{if eq .SortType "fewestforks"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "fewestforks"}}checked{{end}} value="fewestforks"> {{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</label>
+				<label class="{{if eq .SortType "size"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "size"}}checked{{end}} value="size"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.by_size"}}</label>
+				<label class="{{if eq .SortType "reversesize"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "reversesize"}}checked{{end}} value="reversesize"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_by_size"}}</label>
+			</div>
+		</div>
+	</form>
+</div>
+{{if and .PageIsExploreRepositories .OnlyShowRelevant}}
+	<div class="ui message">
+		<span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">
+			{{ctx.Locale.Tr "explore.relevant_repositories" (printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))}}
+		</span>
+	</div>
+{{end}}
+<div class="divider"></div>
diff --git a/templates/shared/search/button.tmpl b/templates/shared/search/button.tmpl
new file mode 100644
index 0000000000..7bb1662e15
--- /dev/null
+++ b/templates/shared/search/button.tmpl
@@ -0,0 +1,3 @@
+{{/* Disable (optional) - if search button has to be disabled */}}
+{{/* Tooltip (optional) - a tooltip to be displayed on hover */}}
+<button class="ui small icon button" aria-label="{{ctx.Locale.Tr "search.search"}}" {{with .Tooltip}}data-tooltip-content="{{.}}"{{end}}{{if .Disabled}} disabled{{end}}>{{svg "octicon-search"}}</button>
diff --git a/templates/shared/search/code/results.tmpl b/templates/shared/search/code/results.tmpl
new file mode 100644
index 0000000000..a98a662654
--- /dev/null
+++ b/templates/shared/search/code/results.tmpl
@@ -0,0 +1,36 @@
+<div class="flex-text-block tw-flex-wrap">
+	{{range $term := .SearchResultLanguages}}
+	<a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label tw-m-0"
+		href="?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}&fuzzy={{$.IsFuzzy}}">
+		<i class="color-icon tw-mr-2" style="background-color: {{$term.Color}}"></i>
+		{{$term.Language}}
+		<div class="detail">{{$term.Count}}</div>
+	</a>
+	{{end}}
+</div>
+<div class="repository search">
+	{{range $result := .SearchResults}}
+		{{$repo := or $.Repo (index $.RepoMaps .RepoID)}}
+		<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
+			<h4 class="ui top attached header tw-font-normal tw-flex tw-flex-wrap">
+				{{if not $.Repo}}
+					<span class="file tw-flex-1">
+						<a rel="nofollow" href="{{$repo.Link}}">{{$repo.FullName}}</a>
+						{{if $repo.IsArchived}}
+							<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.archived"}}</span>
+						{{end}}
+						- {{.Filename}}
+					</span>
+				{{else}}
+					<span class="file tw-flex-1">{{.Filename}}</span>
+				{{end}}
+				<a role="button" class="ui basic tiny button" rel="nofollow" href="{{$repo.Link}}/src/commit/{{$result.CommitID | PathEscape}}/{{.Filename | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
+			</h4>
+			<div class="ui attached table segment">
+				{{template "shared/searchfile" dict "RepoLink" $repo.Link "SearchResult" .}}
+			</div>
+			{{template "shared/searchbottom" dict "root" $ "result" .}}
+		</div>
+	{{end}}
+</div>
+{{template "base/paginate" .}}
diff --git a/templates/shared/search/code/search.tmpl b/templates/shared/search/code/search.tmpl
new file mode 100644
index 0000000000..cb873f5a92
--- /dev/null
+++ b/templates/shared/search/code/search.tmpl
@@ -0,0 +1,22 @@
+<form class="ui form ignore-dirty">
+	{{template "shared/search/combo_fuzzy" dict "Value" .Keyword "Disabled" .CodeIndexerUnavailable "IsFuzzy" .IsFuzzy "Placeholder" (ctx.Locale.Tr "search.code_kind")}}
+</form>
+<div class="divider"></div>
+<div class="ui user list">
+	{{if .CodeIndexerUnavailable}}
+		<div class="ui error message">
+			<p>{{ctx.Locale.Tr "search.code_search_unavailable"}}</p>
+		</div>
+	{{else}}
+		{{if not .CodeIndexerEnabled}}
+			<div class="ui message">
+				<p>{{ctx.Locale.Tr "search.code_search_by_git_grep"}}</p>
+			</div>
+		{{end}}
+		{{if .SearchResults}}
+			{{template "shared/search/code/results" .}}
+		{{else if .Keyword}}
+			<div>{{ctx.Locale.Tr "search.no_results"}}</div>
+		{{end}}
+	{{end}}
+</div>
diff --git a/templates/shared/search/combo.tmpl b/templates/shared/search/combo.tmpl
new file mode 100644
index 0000000000..788db95cc1
--- /dev/null
+++ b/templates/shared/search/combo.tmpl
@@ -0,0 +1,8 @@
+{{/* Value - value of the search field (for search results page) */}}
+{{/* Disabled (optional) - if search field/button has to be disabled */}}
+{{/* Placeholder (optional) - placeholder text to be used */}}
+{{/* Tooltip (optional) - a tooltip to be displayed on button hover */}}
+<div class="ui small fluid action input">
+	{{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}}
+	{{template "shared/search/button" dict "Disabled" .Disabled "Tooltip" .Tooltip}}
+</div>
diff --git a/templates/shared/search/combo_fuzzy.tmpl b/templates/shared/search/combo_fuzzy.tmpl
new file mode 100644
index 0000000000..3540a89ecb
--- /dev/null
+++ b/templates/shared/search/combo_fuzzy.tmpl
@@ -0,0 +1,10 @@
+{{/* Value - value of the search field (for search results page) */}}
+{{/* Disabled (optional) - if search field/button has to be disabled */}}
+{{/* Placeholder (optional) - placeholder text to be used */}}
+{{/* IsFuzzy - state of the fuzzy search toggle */}}
+{{/* Tooltip (optional) - a tooltip to be displayed on button hover */}}
+<div class="ui small fluid action input">
+	{{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}}
+	{{template "shared/search/fuzzy" dict "Disabled" .Disabled "IsFuzzy" .IsFuzzy}}
+	{{template "shared/search/button" dict "Disabled" .Disabled "Tooltip" .Tooltip}}
+</div>
diff --git a/templates/shared/search/fuzzy.tmpl b/templates/shared/search/fuzzy.tmpl
new file mode 100644
index 0000000000..6ddb03c004
--- /dev/null
+++ b/templates/shared/search/fuzzy.tmpl
@@ -0,0 +1,10 @@
+{{/* Disabled (optional) - if dropdown has to be disabled */}}
+{{/* IsFuzzy - state of the fuzzy search toggle */}}
+<div class="ui small dropdown selection {{if .Disabled}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "search.type_tooltip"}}">
+	<input name="fuzzy" type="hidden"{{if .Disabled}} disabled{{end}} value="{{.IsFuzzy}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+	<div class="text">{{if .IsFuzzy}}{{ctx.Locale.Tr "search.fuzzy"}}{{else}}{{ctx.Locale.Tr "search.match"}}{{end}}</div>
+	<div class="menu">
+		<div class="item" data-value="true" data-tooltip-content="{{ctx.Locale.Tr "search.fuzzy_tooltip"}}">{{ctx.Locale.Tr "search.fuzzy"}}</div>
+		<div class="item" data-value="false" data-tooltip-content="{{ctx.Locale.Tr "search.match_tooltip"}}">{{ctx.Locale.Tr "search.match"}}</div>
+	</div>
+</div>
diff --git a/templates/shared/search/input.tmpl b/templates/shared/search/input.tmpl
new file mode 100644
index 0000000000..75bed07b80
--- /dev/null
+++ b/templates/shared/search/input.tmpl
@@ -0,0 +1,4 @@
+{{/* Value - value of the search field (for search results page) */}}
+{{/* Disabled (optional) - if search field has to be disabled */}}
+{{/* Placeholder (optional) - placeholder text to be used */}}
+<input type="search" name="q"{{with .Value}} value="{{.}}"{{end}} maxlength="255" spellcheck="false" placeholder="{{with .Placeholder}}{{.}}{{else}}{{ctx.Locale.Tr "search.search"}}{{end}}"{{if .Disabled}} disabled{{end}}>
diff --git a/templates/shared/searchbottom.tmpl b/templates/shared/searchbottom.tmpl
index 55b6cb2909..bee0397259 100644
--- a/templates/shared/searchbottom.tmpl
+++ b/templates/shared/searchbottom.tmpl
@@ -1,12 +1,14 @@
-<div class="ui bottom attached table segment gt-df gt-ac gt-sb">
-		<div class="gt-df gt-ac gt-ml-4">
+{{if or .result.Language (not .result.UpdatedUnix.IsZero)}}
+<div class="ui bottom attached table segment tw-flex tw-items-center tw-justify-between">
+		<div class="tw-flex tw-items-center tw-ml-4">
 			{{if .result.Language}}
-					<i class="color-icon gt-mr-3" style="background-color: {{.result.Color}}"></i>{{.result.Language}}
+					<i class="color-icon tw-mr-2" style="background-color: {{.result.Color}}"></i>{{.result.Language}}
 			{{end}}
 		</div>
-		<div class="gt-mr-4">
+		<div class="tw-mr-4">
 			{{if not .result.UpdatedUnix.IsZero}}
-					<span class="ui grey text">{{ctx.Locale.Tr "explore.code_last_indexed_at" (TimeSinceUnix .result.UpdatedUnix ctx.Locale) | Safe}}</span>
+					<span class="ui grey text">{{ctx.Locale.Tr "explore.code_last_indexed_at" (TimeSinceUnix .result.UpdatedUnix ctx.Locale)}}</span>
 			{{end}}
 		</div>
 </div>
+{{end}}
diff --git a/templates/shared/searchfile.tmpl b/templates/shared/searchfile.tmpl
new file mode 100644
index 0000000000..280584e4d1
--- /dev/null
+++ b/templates/shared/searchfile.tmpl
@@ -0,0 +1,14 @@
+<div class="file-body file-code code-view">
+	<table>
+		<tbody>
+			{{range .SearchResult.Lines}}
+				<tr>
+					<td class="lines-num">
+						<a href="{{$.RepoLink}}/src/commit/{{PathEscape $.SearchResult.CommitID}}/{{PathEscapeSegments $.SearchResult.Filename}}#L{{.Num}}"><span>{{.Num}}</span></a>
+					</td>
+					<td class="lines-code chroma"><code class="code-inner">{{.FormattedContent}}</code></td>
+				</tr>
+			{{end}}
+		</tbody>
+	</table>
+</div>
diff --git a/templates/shared/searchinput.tmpl b/templates/shared/searchinput.tmpl
deleted file mode 100644
index 48b288c299..0000000000
--- a/templates/shared/searchinput.tmpl
+++ /dev/null
@@ -1 +0,0 @@
-<input type="search" spellcheck="false" name="q" maxlength="255" placeholder="{{ctx.Locale.Tr "explore.search"}}…"{{if .Value}} value="{{.Value}}"{{end}}{{if .Disabled}} disabled{{end}}>
diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl
index 7192f31fb2..ea59459083 100644
--- a/templates/shared/secrets/add_list.tmpl
+++ b/templates/shared/secrets/add_list.tmpl
@@ -14,7 +14,7 @@
 	{{if .Secrets}}
 	<div class="flex-list">
 		{{range .Secrets}}
-		<div class="flex-item gt-ac">
+		<div class="flex-item tw-items-center">
 			<div class="flex-item-leading">
 				{{svg "octicon-key" 32}}
 			</div>
@@ -28,9 +28,9 @@
 			</div>
 			<div class="flex-item-trailing">
 				<span class="color-text-light-2">
-					{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}}
+					{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}}
 				</span>
-				<button class="ui btn interact-bg link-action gt-p-3"
+				<button class="ui btn interact-bg link-action tw-p-2"
 					data-url="{{$.Link}}/delete?id={{.ID}}"
 					data-modal-confirm="{{ctx.Locale.Tr "secrets.deletion.description"}}"
 					data-tooltip-content="{{ctx.Locale.Tr "secrets.deletion"}}"
diff --git a/templates/shared/user/authorlink.tmpl b/templates/shared/user/authorlink.tmpl
index 64ccc62cd0..d57a635b4b 100644
--- a/templates/shared/user/authorlink.tmpl
+++ b/templates/shared/user/authorlink.tmpl
@@ -1 +1 @@
-<a class="author text black gt-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label gt-p-2">bot</span>{{end}}
+<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label tw-p-1">bot</span>{{end}}
diff --git a/templates/shared/user/block_user_dialog.tmpl b/templates/shared/user/block_user_dialog.tmpl
new file mode 100644
index 0000000000..c6db4ca1e4
--- /dev/null
+++ b/templates/shared/user/block_user_dialog.tmpl
@@ -0,0 +1,23 @@
+<div class="ui small modal" id="block-user-modal">
+	<div class="header">{{ctx.Locale.Tr "user.block.title"}}</div>
+	<div class="content">
+		<div class="ui warning message">{{ctx.Locale.Tr "user.block.info"}}</div>
+		<form class="ui form modal-form" method="post">
+			{{.CsrfTokenHtml}}
+			<input type="hidden" name="action" value="block" />
+			<input type="hidden" name="blockee" class="modal-blockee" />
+			<div class="field">
+				<label>{{ctx.Locale.Tr "user.block.user_to_block"}}: <span class="text red modal-blockee-name"></span></label>
+			</div>
+			<div class="field">
+				<label for="block-note">{{ctx.Locale.Tr "user.block.note.title"}}</label>
+				<input id="block-note" name="note">
+				<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
+			</div>
+			<div class="text right actions">
+				<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
+				<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
+			</div>
+		</form>
+	</div>
+</div>
diff --git a/templates/shared/user/blocked_users.tmpl b/templates/shared/user/blocked_users.tmpl
new file mode 100644
index 0000000000..e83a039ef5
--- /dev/null
+++ b/templates/shared/user/blocked_users.tmpl
@@ -0,0 +1,83 @@
+<h4 class="ui top attached header">
+	{{ctx.Locale.Tr "user.block.title"}}
+</h4>
+<div class="ui attached segment">
+	<p>{{ctx.Locale.Tr "user.block.info_1"}}</p>
+	<ul>
+		<li>{{ctx.Locale.Tr "user.block.info_2"}}</li>
+		<li>{{ctx.Locale.Tr "user.block.info_3"}}</li>
+		<li>{{ctx.Locale.Tr "user.block.info_4"}}</li>
+		<li>{{ctx.Locale.Tr "user.block.info_5"}}</li>
+		<li>{{ctx.Locale.Tr "user.block.info_6"}}</li>
+		<li>{{ctx.Locale.Tr "user.block.info_7"}}</li>
+	</ul>
+</div>
+<div class="ui segment">
+	<form class="ui form ignore-dirty" action="{{$.Link}}" method="post">
+		{{.CsrfTokenHtml}}
+		<input type="hidden" name="action" value="block" />
+		<div id="search-user-box" class="field ui fluid search input">
+			<input class="prompt tw-mr-2" name="blockee" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" required>
+			<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
+		</div>
+		<div class="field">
+			<label>{{ctx.Locale.Tr "user.block.note.title"}}</label>
+			<input name="note">
+			<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
+		</div>
+	</form>
+</div>
+<h4 class="ui top attached header">
+	{{ctx.Locale.Tr "user.block.list"}}
+</h4>
+<div class="ui attached segment">
+	<div class="flex-list">
+		{{range .UserBlocks}}
+			<div class="flex-item">
+				<div class="flex-item-leading">
+					{{ctx.AvatarUtils.Avatar .Blockee}}
+				</div>
+				<div class="flex-item-main">
+					<div class="flex-item-title">
+						<a class="item" href="{{.Blockee.HTMLURL}}">{{.Blockee.GetDisplayName}}</a>
+					</div>
+					{{if .Note}}
+					<div class="flex-item-body">
+						<i>{{ctx.Locale.Tr "user.block.note"}}:</i> {{.Note}}
+					</div>
+					{{end}}
+				</div>
+				<div class="flex-item-trailing">
+					<button class="ui compact mini button show-modal" data-modal="#block-user-note-modal" data-modal-modal-blockee="{{.Blockee.Name}}" data-modal-modal-note="{{.Note}}">{{ctx.Locale.Tr "user.block.note.edit"}}</button>
+					<form action="{{$.Link}}" method="post">
+						{{$.CsrfTokenHtml}}
+						<input type="hidden" name="action" value="unblock" />
+						<input type="hidden" name="blockee" value="{{.Blockee.Name}}" />
+						<button class="ui compact mini button">{{ctx.Locale.Tr "user.block.unblock"}}</button>
+					</form>
+				</div>
+			</div>
+		{{else}}
+			<div class="item">{{ctx.Locale.Tr "user.block.list.none"}}</div>
+		{{end}}
+	</div>
+</div>
+<div class="ui small modal" id="block-user-note-modal">
+	<div class="header">{{ctx.Locale.Tr "user.block.note.edit"}}</div>
+	<div class="content">
+		<form class="ui form" action="{{$.Link}}" method="post">
+			{{.CsrfTokenHtml}}
+			<input type="hidden" name="action" value="note" />
+			<input type="hidden" name="blockee" class="modal-blockee" />
+			<div class="field">
+				<label>{{ctx.Locale.Tr "user.block.note.title"}}</label>
+				<input name="note" class="modal-note" />
+				<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
+			</div>
+			<div class="text right actions">
+				<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
+				<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
+			</div>
+		</form>
+	</div>
+</div>
diff --git a/templates/shared/user/org_profile_avatar.tmpl b/templates/shared/user/org_profile_avatar.tmpl
index a8846b0abd..2ff1e40ca8 100644
--- a/templates/shared/user/org_profile_avatar.tmpl
+++ b/templates/shared/user/org_profile_avatar.tmpl
@@ -2,7 +2,7 @@
 	<div class="ui container">
 		<div class="ui vertically grid head">
 			<div class="column">
-				<div class="ui header gt-df gt-ac gt-word-break">
+				<div class="ui header tw-flex tw-items-center gt-word-break">
 					{{ctx.AvatarUtils.Avatar . 100}}
 					<span class="text thin grey"><a href="{{.HomeLink}}">{{.DisplayName}}</a></span>
 					<span class="org-visibility">
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl
index 4fbc43f541..868f8d5a13 100644
--- a/templates/shared/user/profile_big_avatar.tmpl
+++ b/templates/shared/user/profile_big_avatar.tmpl
@@ -1,5 +1,5 @@
 <div id="profile-avatar-card" class="ui card">
-	<div id="profile-avatar" class="content gt-df">
+	<div id="profile-avatar" class="content tw-flex">
 	{{if eq .SignedUserID .ContextUser.ID}}
 		<a class="image" href="{{AppSubUrl}}/user/settings" data-tooltip-content="{{ctx.Locale.Tr "user.change_avatar"}}">
 			{{/* the size doesn't take affect (and no need to take affect), image size(width) should be controlled by the parent container since this is not a flex layout*/}}
@@ -18,22 +18,27 @@
 						{{svg "octicon-gear" 18}}
 					</a>
 				{{end}}</span>
-		<div class="gt-mt-3">
-			<a class="muted" href="{{.ContextUser.HomeLink}}?tab=followers">{{svg "octicon-person" 18 "gt-mr-2"}}{{.NumFollowers}} {{ctx.Locale.Tr "user.followers"}}</a> · <a class="muted" href="{{.ContextUser.HomeLink}}?tab=following">{{.NumFollowing}} {{ctx.Locale.Tr "user.following"}}</a>
+		<div class="tw-mt-2">
+			<a class="muted" href="{{.ContextUser.HomeLink}}?tab=followers">{{svg "octicon-person" 18 "tw-mr-1"}}{{.NumFollowers}} {{ctx.Locale.Tr "user.followers"}}</a> · <a class="muted" href="{{.ContextUser.HomeLink}}?tab=following">{{.NumFollowing}} {{ctx.Locale.Tr "user.following"}}</a>
 			{{if .EnableFeed}}
-				<a href="{{.ContextUser.HomeLink}}.rss"><i class="ui text grey gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">{{svg "octicon-rss" 18}}</i></a>
+				<a href="{{.ContextUser.HomeLink}}.rss"><i class="ui text grey tw-ml-2" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">{{svg "octicon-rss" 18}}</i></a>
 			{{end}}
 		</div>
 	</div>
 	<div class="extra content gt-word-break">
 		<ul>
+			{{if .UserBlocking}}
+				<li class="text red">{{svg "octicon-circle-slash"}} {{ctx.Locale.Tr "user.block.blocked"}}</li>
+				{{if .UserBlocking.Note}}
+					<li class="text small red">{{ctx.Locale.Tr "user.block.note"}}: {{.UserBlocking.Note}}</li>
+				{{end}}
+			{{end}}
 			{{if .ContextUser.Location}}
 				<li>
 					{{svg "octicon-location"}}
-					<span class="gt-f1">{{.ContextUser.Location}}</span>
-					{{if .UserLocationMapURL}}
-						{{/* We presume that the UserLocationMapURL is safe, as it is provided by the site administrator. */}}
-						<a href="{{.UserLocationMapURL | Safe}}{{.ContextUser.Location | QueryEscape}}" rel="nofollow noreferrer" data-tooltip-content="{{ctx.Locale.Tr "user.show_on_map"}}">
+					<span class="tw-flex-1">{{.ContextUser.Location}}</span>
+					{{if .ContextUserLocationMapURL}}
+						<a href="{{.ContextUserLocationMapURL}}" rel="nofollow noreferrer" data-tooltip-content="{{ctx.Locale.Tr "user.show_on_map"}}">
 							{{svg "octicon-link-external"}}
 						</a>
 					{{end}}
@@ -42,7 +47,7 @@
 			{{if (eq .SignedUserID .ContextUser.ID)}}
 				<li>
 					{{svg "octicon-mail"}}
-					<a class="gt-f1" href="mailto:{{.ContextUser.Email}}" rel="nofollow">{{.ContextUser.Email}}</a>
+					<a class="tw-flex-1" href="mailto:{{.ContextUser.Email}}" rel="nofollow">{{.ContextUser.Email}}</a>
 					<a href="{{AppSubUrl}}/user/settings#privacy-user-settings">
 						{{if .ShowUserEmail}}
 							<i data-tooltip-content="{{ctx.Locale.Tr "user.email_visibility.limited"}}">
@@ -71,7 +76,7 @@
 			{{end}}
 			{{if $.RenderedDescription}}
 				<li>
-					<div class="render-content markup">{{$.RenderedDescription|Str2html}}</div>
+					<div class="render-content markup">{{$.RenderedDescription}}</div>
 				</li>
 			{{end}}
 			{{range .OpenIDs}}
@@ -82,7 +87,7 @@
 					</li>
 				{{end}}
 			{{end}}
-			<li>{{svg "octicon-calendar"}} <span>{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .ContextUser.CreatedUnix) | Safe}}</span></li>
+			<li>{{svg "octicon-calendar"}} <span>{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .ContextUser.CreatedUnix)}}</span></li>
 			{{if and .Orgs .HasOrgsVisible}}
 			<li>
 				<ul class="user-orgs">
@@ -110,18 +115,29 @@
 			</li>
 			{{end}}
 			{{if and .IsSigned (ne .SignedUserID .ContextUser.ID)}}
-			<li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card" >
-				{{if $.IsFollowing}}
-					<button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button">
-						{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unfollow"}}
-					</button>
-				{{else}}
-					<button hx-post="{{.ContextUser.HomeLink}}?action=follow" class="ui basic primary button">
-						{{svg "octicon-person"}} {{ctx.Locale.Tr "user.follow"}}
-					</button>
+				{{if not .UserBlocking}}
+				<li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card">
+					{{if $.IsFollowing}}
+						<button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button">
+							{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unfollow"}}
+						</button>
+					{{else}}
+						<button hx-post="{{.ContextUser.HomeLink}}?action=follow" class="ui basic primary button">
+							{{svg "octicon-person"}} {{ctx.Locale.Tr "user.follow"}}
+						</button>
+					{{end}}
+				</li>
 				{{end}}
-			</li>
+				<li>
+					{{if not .UserBlocking}}
+						<a class="muted show-modal" href="#" data-modal="#block-user-modal" data-modal-modal-blockee="{{.ContextUser.Name}}" data-modal-modal-blockee-name="{{.ContextUser.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</a>
+					{{else}}
+						<a class="muted" href="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.unblock"}}</a>
+					{{end}}
+				</li>
 			{{end}}
 		</ul>
 	</div>
 </div>
+
+{{template "shared/user/block_user_dialog" .}}
diff --git a/templates/shared/variables/variable_list.tmpl b/templates/shared/variables/variable_list.tmpl
index fc5cd966fc..06c71c0610 100644
--- a/templates/shared/variables/variable_list.tmpl
+++ b/templates/shared/variables/variable_list.tmpl
@@ -16,7 +16,7 @@
 	{{if .Variables}}
 	<div class="flex-list">
 		{{range .Variables}}
-		<div class="flex-item gt-ac">
+		<div class="flex-item tw-items-center">
 			<div class="flex-item-leading">
 				{{svg "octicon-pencil" 32}}
 			</div>
@@ -30,9 +30,9 @@
 			</div>
 			<div class="flex-item-trailing">
 				<span class="color-text-light-2">
-					{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}}
+					{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}}
 				</span>
-				<button class="btn interact-bg gt-p-3 show-modal"
+				<button class="btn interact-bg tw-p-2 show-modal"
 					data-tooltip-content="{{ctx.Locale.Tr "actions.variables.edit"}}"
 					data-modal="#edit-variable-modal"
 					data-modal-form.action="{{$.Link}}/{{.ID}}/edit"
@@ -42,7 +42,7 @@
 				>
 					{{svg "octicon-pencil"}}
 				</button>
-				<button class="btn interact-bg gt-p-3 link-action"
+				<button class="btn interact-bg tw-p-2 link-action"
 					data-tooltip-content="{{ctx.Locale.Tr "actions.variables.deletion"}}"
 					data-url="{{$.Link}}/{{.ID}}/delete"
 					data-modal-confirm="{{ctx.Locale.Tr "actions.variables.deletion.description"}}"
diff --git a/templates/shared/webhook/icon.tmpl b/templates/shared/webhook/icon.tmpl
index 84f9de266f..0f80787c57 100644
--- a/templates/shared/webhook/icon.tmpl
+++ b/templates/shared/webhook/icon.tmpl
@@ -3,7 +3,7 @@
 	{{$size = .Size}}
 {{end}}
 {{if eq .HookType "gitea"}}
-	<img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gitea.svg">
+	{{svg "gitea-gitea" $size "img"}}
 {{else if eq .HookType "gogs"}}
 	<img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gogs.ico">
 {{else if eq .HookType "slack"}}
diff --git a/templates/status/404.tmpl b/templates/status/404.tmpl
index 74bb8762bd..78f149e67b 100644
--- a/templates/status/404.tmpl
+++ b/templates/status/404.tmpl
@@ -1,14 +1,10 @@
 {{template "base/head" .}}
-<div role="main" aria-label="{{.Title}}" class="page-content ui container center gt-w-screen {{if .IsRepo}}repository{{end}}">
+<div role="main" aria-label="{{.Title}}" class="page-content {{if .IsRepo}}repository{{end}}">
 	{{if .IsRepo}}{{template "repo/header" .}}{{end}}
-	<div class="ui container center">
-		<p style="margin-top: 100px"><img src="{{AssetUrlPrefix}}/img/404.png" alt="404"></p>
-		<p>{{if .NotFoundPrompt}}{{.NotFoundPrompt}}{{else}}{{ctx.Locale.Tr "error404" | Safe}}{{end}}</p>
-		{{if .NotFoundGoBackURL}}<a class="ui button green" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a>{{end}}
-
-		<div class="divider"></div>
-		<br>
-		{{if .ShowFooterVersion}}<p>{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
+	<div class="ui container tw-text-center">
+		<img class="tw-max-w-[80vw] tw-py-16" src="{{AssetUrlPrefix}}/img/404.png" alt="404">
+		<p>{{if .NotFoundPrompt}}{{.NotFoundPrompt}}{{else}}{{ctx.Locale.Tr "error404"}}{{end}}</p>
+		{{if .NotFoundGoBackURL}}<a class="ui button" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a>{{end}}
 	</div>
 </div>
 {{template "base/footer" .}}
diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl
index edcb90f9a4..576b6eebbb 100644
--- a/templates/status/500.tmpl
+++ b/templates/status/500.tmpl
@@ -1,5 +1,5 @@
 {{/* This page should only depend the minimal template functions/variables, to avoid triggering new panics.
-* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName, Str2html
+* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName
 * ctx.Locale
 * .Flash
 * .ErrorMsg
@@ -16,9 +16,9 @@
 </head>
 <body>
 	<div class="full height">
-		<nav class="ui secondary menu gt-border-secondary-bottom">
-			<div class="ui container gt-df">
-				<div class="item gt-f1">
+		<nav class="ui secondary menu">
+			<div class="ui container tw-flex">
+				<div class="item tw-flex-1">
 					<a href="{{AppSubUrl}}/" aria-label="{{ctx.Locale.Tr "home"}}">
 						<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
 					</a>
@@ -28,21 +28,22 @@
 				</div>
 			</div>
 		</nav>
+		<div class="divider tw-my-0"></div>
 		<div role="main" class="page-content status-page-500">
 			<div class="ui container" >
 				<style> .ui.message.flash-message { text-align: left; } </style>
 				{{template "base/alert" .}}
 			</div>
-			<p class="gt-mt-5 center"><img src="{{AssetUrlPrefix}}/img/500.png" alt="Internal Server Error"></p>
+			<p class="tw-mt-8 center"><img src="{{AssetUrlPrefix}}/img/500.png" alt="Internal Server Error"></p>
 			<div class="divider"></div>
-			<div class="ui container gt-my-5">
+			<div class="ui container tw-my-8">
 				{{if .ErrorMsg}}
 					<p>{{ctx.Locale.Tr "error.occurred"}}:</p>
-					<pre class="gt-whitespace-pre-wrap gt-break-all">{{.ErrorMsg}}</pre>
+					<pre class="tw-whitespace-pre-wrap tw-break-all">{{.ErrorMsg}}</pre>
 				{{end}}
-				<div class="center gt-mt-5">
+				<div class="center tw-mt-8">
 					{{if or .SignedUser.IsAdmin .ShowFooterVersion}}<p>{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
-					{{if .SignedUser.IsAdmin}}<p>{{ctx.Locale.Tr "error.report_message" | Str2html}}</p>{{end}}
+					{{if .SignedUser.IsAdmin}}<p>{{ctx.Locale.Tr "error.report_message"}}</p>{{end}}
 				</div>
 			</div>
 		</div>
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 403f241d72..b5677c77e0 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -8,8 +8,8 @@
     "text/html"
   ],
   "schemes": [
-    "http",
-    "https"
+    "https",
+    "http"
   ],
   "swagger": "2.0",
   "info": {
@@ -19,9 +19,9 @@
       "name": "MIT",
       "url": "http://opensource.org/licenses/MIT"
     },
-    "version": "{{AppVer | JSEscape | Safe}}"
+    "version": "{{AppVer | JSEscape}}"
   },
-  "basePath": "{{AppSubUrl | JSEscape | Safe}}/api/v1",
+  "basePath": "{{AppSubUrl | JSEscape}}/api/v1",
   "paths": {
     "/activitypub/user-id/{user-id}": {
       "get": {
@@ -689,6 +689,109 @@
         }
       }
     },
+    "/admin/users/{username}/badges": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "admin"
+        ],
+        "summary": "List a user's badges",
+        "operationId": "adminListUserBadges",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "username of user",
+            "name": "username",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/BadgeList"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "post": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "admin"
+        ],
+        "summary": "Add a badge to a user",
+        "operationId": "adminAddUserBadges",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "username of user",
+            "name": "username",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/UserBadgeOption"
+            }
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          }
+        }
+      },
+      "delete": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "admin"
+        ],
+        "summary": "Remove a badge from a user",
+        "operationId": "adminDeleteUserBadges",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "username of user",
+            "name": "username",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/UserBadgeOption"
+            }
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      }
+    },
     "/admin/users/{username}/keys": {
       "post": {
         "consumes": [
@@ -1741,6 +1844,232 @@
         }
       }
     },
+    "/orgs/{org}/actions/variables": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "Get an org-level variables list",
+        "operationId": "getOrgVariablesList",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results",
+            "name": "limit",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/VariableList"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
+    "/orgs/{org}/actions/variables/{variablename}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "Get an org-level variable",
+        "operationId": "getOrgVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ActionVariable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "put": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "Update an org-level variable",
+        "operationId": "updateOrgVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/UpdateVariableOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when updating an org-level variable"
+          },
+          "204": {
+            "description": "response when updating an org-level variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "post": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "Create an org-level variable",
+        "operationId": "createOrgVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/CreateVariableOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when creating an org-level variable"
+          },
+          "204": {
+            "description": "response when creating an org-level variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "delete": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "Delete an org-level variable",
+        "operationId": "deleteOrgVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ActionVariable"
+          },
+          "201": {
+            "description": "response when deleting a variable"
+          },
+          "204": {
+            "description": "response when deleting a variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/orgs/{org}/activities/feeds": {
       "get": {
         "produces": [
@@ -1852,6 +2181,151 @@
         }
       }
     },
+    "/orgs/{org}/blocks": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "List users blocked by the organization",
+        "operationId": "organizationListBlocks",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results",
+            "name": "limit",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/UserList"
+          }
+        }
+      }
+    },
+    "/orgs/{org}/blocks/{username}": {
+      "get": {
+        "tags": [
+          "organization"
+        ],
+        "summary": "Check if a user is blocked by the organization",
+        "operationId": "organizationCheckUserBlock",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "user to check",
+            "name": "username",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "put": {
+        "tags": [
+          "organization"
+        ],
+        "summary": "Block a user",
+        "operationId": "organizationBlockUser",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "user to block",
+            "name": "username",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "optional note for the block",
+            "name": "note",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      },
+      "delete": {
+        "tags": [
+          "organization"
+        ],
+        "summary": "Unblock a user",
+        "operationId": "organizationUnblockUser",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "user to unblock",
+            "name": "username",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      }
+    },
     "/orgs/{org}/hooks": {
       "get": {
         "produces": [
@@ -3475,6 +3949,261 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/actions/variables": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Get repo-level variables list",
+        "operationId": "getRepoVariablesList",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results",
+            "name": "limit",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/VariableList"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
+    "/repos/{owner}/{repo}/actions/variables/{variablename}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Get a repo-level variable",
+        "operationId": "getRepoVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ActionVariable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "put": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Update a repo-level variable",
+        "operationId": "updateRepoVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/UpdateVariableOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when updating a repo-level variable"
+          },
+          "204": {
+            "description": "response when updating a repo-level variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "post": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Create a repo-level variable",
+        "operationId": "createRepoVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/CreateVariableOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when creating a repo-level variable"
+          },
+          "204": {
+            "description": "response when creating a repo-level variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "delete": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Delete a repo-level variable",
+        "operationId": "deleteRepoVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ActionVariable"
+          },
+          "201": {
+            "description": "response when deleting a variable"
+          },
+          "204": {
+            "description": "response when deleting a variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/activities/feeds": {
       "get": {
         "produces": [
@@ -4237,6 +4966,9 @@
           "204": {
             "$ref": "#/responses/empty"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           },
@@ -4565,6 +5297,49 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/commits/{sha}/pull": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Get the pull request of the commit",
+        "operationId": "repoGetCommitPullRequest",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "SHA of the commit to get",
+            "name": "sha",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/PullRequest"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/contents": {
       "get": {
         "produces": [
@@ -6134,7 +6909,7 @@
           },
           {
             "type": "string",
-            "description": "Only show items which were created by the the given user",
+            "description": "Only show items which were created by the given user",
             "name": "created_by",
             "in": "query"
           },
@@ -6546,6 +7321,9 @@
           "400": {
             "$ref": "#/responses/error"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/error"
           },
@@ -10315,6 +11093,9 @@
           "201": {
             "$ref": "#/responses/PullRequest"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           },
@@ -10366,6 +11147,56 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/pulls/{base}/{head}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Get a pull request by base and head",
+        "operationId": "repoGetPullRequestByBaseHead",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "base of the pull request to get",
+            "name": "base",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "head of the pull request to get",
+            "name": "head",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/PullRequest"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/pulls/{index}": {
       "get": {
         "produces": [
@@ -10926,6 +11757,9 @@
           "204": {
             "$ref": "#/responses/empty"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           },
@@ -12147,7 +12981,8 @@
       },
       "post": {
         "consumes": [
-          "multipart/form-data"
+          "multipart/form-data",
+          "application/octet-stream"
         ],
         "produces": [
           "application/json"
@@ -12190,8 +13025,7 @@
             "type": "file",
             "description": "attachment to upload",
             "name": "attachment",
-            "in": "formData",
-            "required": true
+            "in": "formData"
           }
         ],
         "responses": {
@@ -12763,6 +13597,9 @@
           "200": {
             "$ref": "#/responses/WatchInfo"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           }
@@ -14317,6 +15154,9 @@
           "204": {
             "$ref": "#/responses/empty"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           }
@@ -14691,6 +15531,194 @@
         }
       }
     },
+    "/user/actions/variables": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "Get the user-level list of variables which is created by current doer",
+        "operationId": "getUserVariablesList",
+        "parameters": [
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results",
+            "name": "limit",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/VariableList"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
+    "/user/actions/variables/{variablename}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "Get a user-level variable which is created by current doer",
+        "operationId": "getUserVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ActionVariable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "put": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "Update a user-level variable which is created by current doer",
+        "operationId": "updateUserVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/UpdateVariableOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when updating a variable"
+          },
+          "204": {
+            "description": "response when updating a variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "post": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "Create a user-level variable",
+        "operationId": "createUserVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/CreateVariableOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when creating a variable"
+          },
+          "204": {
+            "description": "response when creating a variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "delete": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "Delete a user-level variable which is created by current doer",
+        "operationId": "deleteUserVariable",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the variable",
+            "name": "variablename",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when deleting a variable"
+          },
+          "204": {
+            "description": "response when deleting a variable"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/user/applications/oauth2": {
       "get": {
         "produces": [
@@ -14885,6 +15913,123 @@
         }
       }
     },
+    "/user/blocks": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "List users blocked by the authenticated user",
+        "operationId": "userListBlocks",
+        "parameters": [
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results",
+            "name": "limit",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/UserList"
+          }
+        }
+      }
+    },
+    "/user/blocks/{username}": {
+      "get": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Check if a user is blocked by the authenticated user",
+        "operationId": "userCheckUserBlock",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "user to check",
+            "name": "username",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "put": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Block a user",
+        "operationId": "userBlockUser",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "user to block",
+            "name": "username",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "optional note for the block",
+            "name": "note",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      },
+      "delete": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Unblock a user",
+        "operationId": "userUnblockUser",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "user to unblock",
+            "name": "username",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      }
+    },
     "/user/emails": {
       "get": {
         "produces": [
@@ -15062,6 +16207,9 @@
           "204": {
             "$ref": "#/responses/empty"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           }
@@ -15769,6 +16917,9 @@
           "204": {
             "$ref": "#/responses/empty"
           },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
           "404": {
             "$ref": "#/responses/notFound"
           }
@@ -16711,6 +17862,35 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "ActionVariable": {
+      "description": "ActionVariable return value of the query API",
+      "type": "object",
+      "properties": {
+        "data": {
+          "description": "the value of the variable",
+          "type": "string",
+          "x-go-name": "Data"
+        },
+        "name": {
+          "description": "the name of the variable",
+          "type": "string",
+          "x-go-name": "Name"
+        },
+        "owner_id": {
+          "description": "the owner to which the variable belongs",
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "OwnerID"
+        },
+        "repo_id": {
+          "description": "the repository to which the variable belongs",
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "RepoID"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "Activity": {
       "type": "object",
       "properties": {
@@ -16910,6 +18090,30 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "Badge": {
+      "description": "Badge represents a user badge",
+      "type": "object",
+      "properties": {
+        "description": {
+          "type": "string",
+          "x-go-name": "Description"
+        },
+        "id": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "ID"
+        },
+        "image_url": {
+          "type": "string",
+          "x-go-name": "ImageURL"
+        },
+        "slug": {
+          "type": "string",
+          "x-go-name": "Slug"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "Branch": {
       "description": "Branch represents a repository branch",
       "type": "object",
@@ -18573,6 +19777,21 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "CreateVariableOption": {
+      "description": "CreateVariableOption the option when creating variable",
+      "type": "object",
+      "required": [
+        "value"
+      ],
+      "properties": {
+        "value": {
+          "description": "Value of the variable to create",
+          "type": "string",
+          "x-go-name": "Value"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "CreateWikiPageOptions": {
       "description": "CreateWikiPageOptions form for creating wiki",
       "type": "object",
@@ -19195,6 +20414,11 @@
       "description": "EditRepoOption options when editing a repository's properties",
       "type": "object",
       "properties": {
+        "allow_fast_forward_only_merge": {
+          "description": "either `true` to allow fast-forward-only merging pull requests, or `false` to prevent fast-forward-only merging.",
+          "type": "boolean",
+          "x-go-name": "AllowFastForwardOnly"
+        },
         "allow_manual_merge": {
           "description": "either `true` to allow mark pr as merged manually, or `false` to prevent it.",
           "type": "boolean",
@@ -19251,7 +20475,7 @@
           "x-go-name": "DefaultDeleteBranchAfterMerge"
         },
         "default_merge_style": {
-          "description": "set to a merge style to be used by this repository: \"merge\", \"rebase\", \"rebase-merge\", or \"squash\".",
+          "description": "set to a merge style to be used by this repository: \"merge\", \"rebase\", \"rebase-merge\", \"squash\", or \"fast-forward-only\".",
           "type": "string",
           "x-go-name": "DefaultMergeStyle"
         },
@@ -19330,6 +20554,11 @@
           "type": "boolean",
           "x-go-name": "Private"
         },
+        "projects_mode": {
+          "description": "`repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.",
+          "type": "string",
+          "x-go-name": "ProjectsMode"
+        },
         "template": {
           "description": "either `true` to make this repository a template or `false` to make it a normal repository",
           "type": "boolean",
@@ -20383,6 +21612,13 @@
           "type": "object",
           "additionalProperties": {},
           "x-go-name": "Validations"
+        },
+        "visible": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/IssueFormFieldVisible"
+          },
+          "x-go-name": "Visible"
         }
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
@@ -20392,6 +21628,11 @@
       "title": "IssueFormFieldType defines issue form field type, can be \"markdown\", \"textarea\", \"input\", \"dropdown\" or \"checkboxes\"",
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "IssueFormFieldVisible": {
+      "description": "IssueFormFieldVisible defines issue form field visible",
+      "type": "string",
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "IssueLabelsOption": {
       "description": "IssueLabelsOption a collection of labels",
       "type": "object",
@@ -20650,6 +21891,7 @@
             "rebase",
             "rebase-merge",
             "squash",
+            "fast-forward-only",
             "manually-merged"
           ]
         },
@@ -22036,6 +23278,10 @@
       "description": "Repository represents a repository",
       "type": "object",
       "properties": {
+        "allow_fast_forward_only_merge": {
+          "type": "boolean",
+          "x-go-name": "AllowFastForwardOnly"
+        },
         "allow_merge_commits": {
           "type": "boolean",
           "x-go-name": "AllowMerge"
@@ -22234,6 +23480,10 @@
           "type": "boolean",
           "x-go-name": "Private"
         },
+        "projects_mode": {
+          "type": "string",
+          "x-go-name": "ProjectsMode"
+        },
         "release_counter": {
           "type": "integer",
           "format": "int64",
@@ -22834,6 +24084,26 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "UpdateVariableOption": {
+      "description": "UpdateVariableOption the option when updating variable",
+      "type": "object",
+      "required": [
+        "value"
+      ],
+      "properties": {
+        "name": {
+          "description": "New name for the variable. If the field is empty, the variable name won't be updated.",
+          "type": "string",
+          "x-go-name": "Name"
+        },
+        "value": {
+          "description": "Value of the variable to update",
+          "type": "string",
+          "x-go-name": "Value"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "User": {
       "description": "User represents a user",
       "type": "object",
@@ -22944,6 +24214,24 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "UserBadgeOption": {
+      "description": "UserBadgeOption options for link between users and badges",
+      "type": "object",
+      "properties": {
+        "badge_slugs": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "BadgeSlugs",
+          "example": [
+            "badge1",
+            "badge2"
+          ]
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "UserHeatmapData": {
       "description": "UserHeatmapData represents the data needed to create a heatmap",
       "type": "object",
@@ -23197,6 +24485,12 @@
         }
       }
     },
+    "ActionVariable": {
+      "description": "ActionVariable",
+      "schema": {
+        "$ref": "#/definitions/ActionVariable"
+      }
+    },
     "ActivityFeedsList": {
       "description": "ActivityFeedsList",
       "schema": {
@@ -23233,6 +24527,15 @@
         }
       }
     },
+    "BadgeList": {
+      "description": "BadgeList",
+      "schema": {
+        "type": "array",
+        "items": {
+          "$ref": "#/definitions/Badge"
+        }
+      }
+    },
     "Branch": {
       "description": "Branch",
       "schema": {
@@ -24071,6 +25374,15 @@
         }
       }
     },
+    "VariableList": {
+      "description": "VariableList",
+      "schema": {
+        "type": "array",
+        "items": {
+          "$ref": "#/definitions/ActionVariable"
+        }
+      }
+    },
     "WatchInfo": {
       "description": "WatchInfo",
       "schema": {
@@ -24146,7 +25458,7 @@
     "parameterBodies": {
       "description": "parameterBodies",
       "schema": {
-        "$ref": "#/definitions/CreateOrUpdateSecretOption"
+        "$ref": "#/definitions/UpdateVariableOption"
       }
     },
     "redirect": {
diff --git a/templates/user/auth/activate.tmpl b/templates/user/auth/activate.tmpl
index 1b06719753..7f8ff0eb5a 100644
--- a/templates/user/auth/activate.tmpl
+++ b/templates/user/auth/activate.tmpl
@@ -2,47 +2,35 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user activate">
 	<div class="ui middle very relaxed page grid">
 		<div class="column">
-			<form class="ui form ignore-dirty" action="{{AppSubUrl}}/user/activate" method="post">
+			<form class="ui form ignore-dirty tw-max-w-2xl tw-m-auto" action="{{AppSubUrl}}/user/activate" method="post">
 				{{.CsrfTokenHtml}}
 				<h2 class="ui top attached header">
 					{{ctx.Locale.Tr "auth.active_your_account"}}
 				</h2>
 				<div class="ui attached segment">
 					{{template "base/alert" .}}
-					{{if .IsActivatePage}}
-						{{if .ServiceNotEnabled}}
-							<p class="center">{{ctx.Locale.Tr "auth.disable_register_mail"}}</p>
-						{{else if .ResendLimited}}
-							<p class="center">{{ctx.Locale.Tr "auth.resent_limit_prompt"}}</p>
-						{{else}}
-							<p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" (.SignedUser.Email|Escape) .ActiveCodeLives | Str2html}}</p>
-						{{end}}
+					{{if .NeedVerifyLocalPassword}}
+						<div class="required field">
+							<label for="verify-password">{{ctx.Locale.Tr "password"}}</label>
+							<input id="verify-password" name="password" type="password" autocomplete="off" required>
+						</div>
+						<div class="inline field">
+							<button class="ui primary button">{{ctx.Locale.Tr "install.confirm_password"}}</button>
+						</div>
+						<input name="code" type="hidden" value="{{.ActivationCode}}">
 					{{else}}
-						{{if .NeedsPassword}}
-							<div class="required inline field">
-								<label for="password">{{ctx.Locale.Tr "password"}}</label>
-								<input id="password" name="password" type="password" autocomplete="off" required>
+						<p>{{ctx.Locale.Tr "auth.has_unconfirmed_mail" .SignedUser.Name .SignedUser.Email}}</p>
+						<details>
+							<summary>{{ctx.Locale.Tr "auth.change_unconfirmed_mail_address"}}</summary>
+							<div class="tw-py-2">
+								<label for="change-email">{{ctx.Locale.Tr "email"}}</label>
+								<input id="change-email" name="change_email" type="email" value="{{.SignedUser.Email}}">
 							</div>
-							<div class="inline field">
-								<label></label>
-								<button class="ui primary button">{{ctx.Locale.Tr "install.confirm_password"}}</button>
-							</div>
-							<input id="code" name="code" type="hidden" value="{{.Code}}">
-						{{else if .IsSendRegisterMail}}
-							<p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" (.Email|Escape) .ActiveCodeLives | Str2html}}</p>
-						{{else if .IsCodeInvalid}}
-							<p>{{ctx.Locale.Tr "auth.invalid_code"}}</p>
-						{{else if .IsPasswordInvalid}}
-							<p>{{ctx.Locale.Tr "auth.invalid_password"}}</p>
-						{{else if .ManualActivationOnly}}
-							<p class="center">{{ctx.Locale.Tr "auth.manual_activation_only"}}</p>
-						{{else}}
-							<p>{{ctx.Locale.Tr "auth.has_unconfirmed_mail" (.SignedUser.Name|Escape) (.SignedUser.Email|Escape) | Str2html}}</p>
-							<div class="divider"></div>
-							<div class="text right">
-								<button class="ui primary button">{{ctx.Locale.Tr "auth.resend_mail"}}</button>
-							</div>
-						{{end}}
+						</details>
+						<div class="divider"></div>
+						<div class="text">
+							<button class="ui primary button">{{ctx.Locale.Tr "auth.resend_mail"}}</button>
+						</div>
 					{{end}}
 				</div>
 			</form>
diff --git a/templates/user/auth/activate_prompt.tmpl b/templates/user/auth/activate_prompt.tmpl
new file mode 100644
index 0000000000..237244df8c
--- /dev/null
+++ b/templates/user/auth/activate_prompt.tmpl
@@ -0,0 +1,15 @@
+{{template "base/head" .}}
+<div role="main" aria-label="{{.Title}}" class="page-content user activate">
+	<div class="ui middle very relaxed page grid">
+		<div class="column">
+			<h2 class="ui top attached header">
+				{{ctx.Locale.Tr "auth.active_your_account"}}
+			</h2>
+			<div class="ui attached segment">
+				{{template "base/alert" .}}
+				<p>{{.ActivationPromptMessage}}</p>
+			</div>
+		</div>
+	</div>
+</div>
+{{template "base/footer" .}}
diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl
index 1c3379e629..0e9c2b9d22 100644
--- a/templates/user/auth/captcha.tmpl
+++ b/templates/user/auth/captcha.tmpl
@@ -1,9 +1,8 @@
 {{if .EnableCaptcha}}{{if eq .CaptchaType "image"}}
 	<div class="inline field">
-		<label>{{/* This is CAPTCHA field */}}</label>
 		{{.Captcha.CreateHTML}}
 	</div>
-	<div class="required inline field {{if .Err_Captcha}}error{{end}}">
+	<div class="required field {{if .Err_Captcha}}error{{end}}">
 		<label for="captcha">{{ctx.Locale.Tr "captcha"}}</label>
 		<input id="captcha" name="captcha" value="{{.captcha}}" autocomplete="off">
 	</div>
@@ -24,7 +23,7 @@
 		<div id="captcha" data-captcha-type="m-captcha" data-sitekey="{{.McaptchaSitekey}}" data-instance-url="{{.McaptchaURL}}"></div>
 	</div>
 {{else if eq .CaptchaType "cfturnstile"}}
-	<div class="inline field gt-text-center">
+	<div class="inline field tw-text-center">
 		<div id="captcha" data-captcha-type="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
 	</div>
 	<script src='https://challenges.cloudflare.com/turnstile/v0/api.js'></script>
diff --git a/templates/user/auth/change_passwd_inner.tmpl b/templates/user/auth/change_passwd_inner.tmpl
index cffc798a64..01bbf500e5 100644
--- a/templates/user/auth/change_passwd_inner.tmpl
+++ b/templates/user/auth/change_passwd_inner.tmpl
@@ -5,18 +5,17 @@
 			{{ctx.Locale.Tr "settings.change_password"}}
 		</h4>
 		<div class="ui attached segment">
-			<form class="ui form" action="{{.ChangePasscodeLink}}" method="post">
+			<form class="ui form tw-max-w-2xl tw-m-auto" action="{{.ChangePasscodeLink}}" method="post">
 			{{.CsrfTokenHtml}}
-			<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
+			<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
 				<label for="password">{{ctx.Locale.Tr "password"}}</label>
 				<input id="password" name="password" type="password" value="{{.password}}" autocomplete="new-password" required>
 			</div>
-			<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
+			<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 				<label for="retype">{{ctx.Locale.Tr "re_type"}}</label>
 				<input id="retype" name="retype" type="password" autocomplete="new-password" required>
 			</div>
 			<div class="inline field">
-				<label></label>
 				<button class="ui primary button">{{ctx.Locale.Tr "settings.change_password"}}</button>
 			</div>
 			</form>
diff --git a/templates/user/auth/finalize_openid.tmpl b/templates/user/auth/finalize_openid.tmpl
index 7449e3beda..1c1dcdb825 100644
--- a/templates/user/auth/finalize_openid.tmpl
+++ b/templates/user/auth/finalize_openid.tmpl
@@ -35,7 +35,7 @@
 					{{if .ShowRegistrationButton}}
 						<div class="inline field">
 							<label></label>
-							<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now" | Str2html}}</a>
+							<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now"}}</a>
 						</div>
 					{{end}}
 					</form>
diff --git a/templates/user/auth/forgot_passwd.tmpl b/templates/user/auth/forgot_passwd.tmpl
index dde4c8f6fe..55bcf63018 100644
--- a/templates/user/auth/forgot_passwd.tmpl
+++ b/templates/user/auth/forgot_passwd.tmpl
@@ -10,15 +10,14 @@
 				<div class="ui attached segment">
 					{{template "base/alert" .}}
 					{{if .IsResetSent}}
-						<p>{{ctx.Locale.Tr "auth.reset_password_mail_sent_prompt" (Escape .Email) .ResetPwdCodeLives | Str2html}}</p>
+						<p>{{ctx.Locale.Tr "auth.reset_password_mail_sent_prompt" .Email .ResetPwdCodeLives}}</p>
 					{{else if .IsResetRequest}}
-						<div class="required inline field {{if .Err_Email}}error{{end}}">
+						<div class="required field {{if .Err_Email}}error{{end}}">
 							<label for="email">{{ctx.Locale.Tr "email"}}</label>
 							<input id="email" name="email" type="email"  value="{{.Email}}" autofocus required>
 						</div>
 						<div class="divider"></div>
 						<div class="inline field">
-							<label></label>
 							<button class="ui primary button">{{ctx.Locale.Tr "auth.send_reset_mail"}}</button>
 						</div>
 					{{else if .IsResetDisable}}
diff --git a/templates/user/auth/grant.tmpl b/templates/user/auth/grant.tmpl
index 9c0bf33e28..cb9bba8749 100644
--- a/templates/user/auth/grant.tmpl
+++ b/templates/user/auth/grant.tmpl
@@ -9,11 +9,11 @@
 				{{template "base/alert" .}}
 				<p>
 					<b>{{ctx.Locale.Tr "auth.authorize_application_description"}}</b><br>
-					{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML | Str2html}}
+					{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}
 				</p>
 			</div>
 			<div class="ui attached segment">
-				<p>{{ctx.Locale.Tr "auth.authorize_redirect_notice" .ApplicationRedirectDomainHTML | Str2html}}</p>
+				<p>{{ctx.Locale.Tr "auth.authorize_redirect_notice" .ApplicationRedirectDomainHTML}}</p>
 			</div>
 			<div class="ui attached segment">
 				<form method="post" action="{{AppSubUrl}}/login/oauth/grant">
diff --git a/templates/user/auth/link_account.tmpl b/templates/user/auth/link_account.tmpl
index 5235cbf69f..8dd49ccd60 100644
--- a/templates/user/auth/link_account.tmpl
+++ b/templates/user/auth/link_account.tmpl
@@ -1,7 +1,7 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content user link-account">
-	<div class="ui secondary pointing tabular top attached borderless menu new-menu navbar">
-		<div class="new-menu-inner">
+	<overflow-menu class="ui secondary pointing tabular top attached borderless menu secondary-nav">
+		<div class="overflow-menu-items tw-justify-center">
 			<!-- TODO handle .ShowRegistrationButton once other login bugs are fixed -->
 			{{if not .AllowOnlyInternalRegistration}}
 				<a class="item {{if not .user_exists}}active{{end}}"
@@ -14,7 +14,7 @@
 				{{ctx.Locale.Tr "auth.oauth_signin_tab"}}
 			</a>
 		</div>
-	</div>
+	</overflow-menu>
 	<div class="ui middle very relaxed page grid">
 		<div class="column">
 			<div class="ui tab {{if not .user_exists}}active{{end}}"
diff --git a/templates/user/auth/oidc_wellknown.tmpl b/templates/user/auth/oidc_wellknown.tmpl
index 38e6900c38..54bb4a763d 100644
--- a/templates/user/auth/oidc_wellknown.tmpl
+++ b/templates/user/auth/oidc_wellknown.tmpl
@@ -1,16 +1,16 @@
 {
-    "issuer": "{{AppUrl | JSEscape | Safe}}",
-    "authorization_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/authorize",
-    "token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token",
-    "jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys",
-    "userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo",
-    "introspection_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/introspect",
+    "issuer": "{{AppUrl | JSEscape}}",
+    "authorization_endpoint": "{{AppUrl | JSEscape}}login/oauth/authorize",
+    "token_endpoint": "{{AppUrl | JSEscape}}login/oauth/access_token",
+    "jwks_uri": "{{AppUrl | JSEscape}}login/oauth/keys",
+    "userinfo_endpoint": "{{AppUrl | JSEscape}}login/oauth/userinfo",
+    "introspection_endpoint": "{{AppUrl | JSEscape}}login/oauth/introspect",
     "response_types_supported": [
         "code",
         "id_token"
     ],
     "id_token_signing_alg_values_supported": [
-        "{{.SigningKey.SigningMethod.Alg | JSEscape | Safe}}"
+        "{{.SigningKey.SigningMethod.Alg | JSEscape}}"
     ],
     "subject_types_supported": [
         "public"
diff --git a/templates/user/auth/prohibit_login.tmpl b/templates/user/auth/prohibit_login.tmpl
index 668aa20e71..962ddfa98c 100644
--- a/templates/user/auth/prohibit_login.tmpl
+++ b/templates/user/auth/prohibit_login.tmpl
@@ -2,7 +2,7 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user activate">
 	<div class="ui middle very relaxed page grid">
 		<div class="column">
-			<form class="ui form">
+			<form class="ui form tw-max-w-2xl tw-m-auto">
 				<h2 class="ui top attached header">
 					{{ctx.Locale.Tr "auth.prohibit_login"}}
 				</h2>
diff --git a/templates/user/auth/reset_passwd.tmpl b/templates/user/auth/reset_passwd.tmpl
index 2f470df441..f8303feef3 100644
--- a/templates/user/auth/reset_passwd.tmpl
+++ b/templates/user/auth/reset_passwd.tmpl
@@ -17,13 +17,12 @@
 						</div>
 					{{end}}
 					{{if .IsResetForm}}
-						<div class="required inline field {{if .Err_Password}}error{{end}}">
+						<div class="required field {{if .Err_Password}}error{{end}}">
 							<label for="password">{{ctx.Locale.Tr "settings.new_password"}}</label>
 							<input id="password" name="password" type="password"  value="{{.password}}" autocomplete="new-password" autofocus required>
 						</div>
 						{{if not .user_signed_in}}
 						<div class="inline field">
-							<label></label>
 							<div class="ui checkbox">
 								<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
 								<input name="remember" type="checkbox">
@@ -34,7 +33,7 @@
 						<h4 class="ui dividing header">
 							{{ctx.Locale.Tr "twofa"}}
 						</h4>
-						<div class="ui warning visible message">{{ctx.Locale.Tr "settings.twofa_is_enrolled" | Str2html}}</div>
+						<div class="ui warning visible message">{{ctx.Locale.Tr "settings.twofa_is_enrolled"}}</div>
 						{{if .scratch_code}}
 						<div class="required inline field {{if .Err_Token}}error{{end}}">
 							<label for="token">{{ctx.Locale.Tr "auth.scratch_code"}}</label>
@@ -42,7 +41,7 @@
 						</div>
 						<input type="hidden" name="scratch_code" value="true">
 						{{else}}
-						<div class="required inline field {{if .Err_Passcode}}error{{end}}">
+						<div class="required field {{if .Err_Passcode}}error{{end}}">
 							<label for="passcode">{{ctx.Locale.Tr "passcode"}}</label>
 							<input id="passcode" name="passcode" type="number" autocomplete="off" autofocus required>
 						</div>
@@ -50,14 +49,13 @@
 						{{end}}
 						<div class="divider"></div>
 						<div class="inline field">
-							<label></label>
 							<button class="ui primary button">{{ctx.Locale.Tr "auth.reset_password_helper"}}</button>
 							{{if and .has_two_factor (not .scratch_code)}}
-								<a href="{{.Link}}?code={{.Code}}&amp;scratch_code=true">{{ctx.Locale.Tr "auth.use_scratch_code" | Str2html}}</a>
+								<a href="?code={{.Code}}&scratch_code=true">{{ctx.Locale.Tr "auth.use_scratch_code"}}</a>
 							{{end}}
 						</div>
 					{{else}}
-						<p class="center">{{ctx.Locale.Tr "auth.invalid_code_forgot_password" (printf "%s/user/forgot_password" AppSubUrl) | Str2html}}</p>
+						<p class="center">{{ctx.Locale.Tr "auth.invalid_code_forgot_password" (printf "%s/user/forgot_password" AppSubUrl)}}</p>
 					{{end}}
 				</div>
 			</form>
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 40e54ec8fa..9872096fbc 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -9,21 +9,20 @@
 	{{end}}
 </h4>
 <div class="ui attached segment">
-	<form class="ui form" action="{{.SignInLink}}" method="post">
+	<form class="ui form tw-max-w-2xl tw-m-auto" action="{{.SignInLink}}" method="post">
 	{{.CsrfTokenHtml}}
-	<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
+	<div class="required field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
 		<label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label>
 		<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
 	</div>
 	{{if or (not .DisablePassword) .LinkAccountMode}}
-	<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
+	<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
 		<label for="password">{{ctx.Locale.Tr "password"}}</label>
 		<input id="password" name="password" type="password" value="{{.password}}" autocomplete="current-password" required>
 	</div>
 	{{end}}
 	{{if not .LinkAccountMode}}
 	<div class="inline field">
-		<label></label>
 		<div class="ui checkbox">
 			<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
 			<input name="remember" type="checkbox">
@@ -33,8 +32,7 @@
 
 	{{template "user/auth/captcha" .}}
 
-	<div class="inline field">
-		<label></label>
+	<div class="field">
 		<button class="ui primary button">
 			{{if .LinkAccountMode}}
 				{{ctx.Locale.Tr "auth.oauth_signin_submit"}}
@@ -46,9 +44,8 @@
 	</div>
 
 	{{if .ShowRegistrationButton}}
-		<div class="inline field">
-			<label></label>
-			<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now" | Str2html}}</a>
+		<div class="field">
+			<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now"}}</a>
 		</div>
 	{{end}}
 
@@ -56,11 +53,11 @@
 	<div class="divider divider-text">
 		{{ctx.Locale.Tr "sign_in_or"}}
 	</div>
-	<div id="oauth2-login-navigator" class="gt-py-2">
-		<div class="gt-df gt-fc gt-jc">
-			<div id="oauth2-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3">
+	<div id="oauth2-login-navigator" class="tw-py-1">
+		<div class="tw-flex tw-flex-col tw-justify-center">
+			<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center tw-gap-2">
 				{{range $provider := .OAuth2Providers}}
-					<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+					<a class="{{$provider.Name}} ui button tw-flex tw-items-center tw-justify-center tw-py-2 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 						{{$provider.IconHTML 28}}
 						{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 					</a>
diff --git a/templates/user/auth/signin_navbar.tmpl b/templates/user/auth/signin_navbar.tmpl
index bc7fd03e13..7f52185a7d 100644
--- a/templates/user/auth/signin_navbar.tmpl
+++ b/templates/user/auth/signin_navbar.tmpl
@@ -1,6 +1,6 @@
 {{if or .EnableOpenIDSignIn .EnableSSPI}}
-<div class="ui secondary pointing tabular top attached borderless menu new-menu navbar">
-	<div class="new-menu-inner">
+<overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar secondary-nav">
+	<div class="overflow-menu-items tw-justify-center">
 		<a class="{{if .PageIsLogin}}active {{end}}item" rel="nofollow" href="{{AppSubUrl}}/user/login">
 			{{ctx.Locale.Tr "auth.login_userpass"}}
 		</a>
@@ -20,5 +20,5 @@
 		</a>
 		{{end}}
 	</div>
-</div>
+</overflow-menu>
 {{end}}
diff --git a/templates/user/auth/signin_openid.tmpl b/templates/user/auth/signin_openid.tmpl
index 0428026aa8..c1f392dc13 100644
--- a/templates/user/auth/signin_openid.tmpl
+++ b/templates/user/auth/signin_openid.tmpl
@@ -8,12 +8,12 @@
 			OpenID
 		</h4>
 		<div class="ui attached segment">
-			<form class="ui form" action="{{.Link}}" method="post">
+			<form class="ui form tw-m-auto" action="{{.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="inline field">
 				{{ctx.Locale.Tr "auth.openid_signin_desc"}}
 			</div>
-			<div class="required inline field {{if .Err_OpenID}}error{{end}}">
+			<div class="required field {{if .Err_OpenID}}error{{end}}">
 				<label for="openid">
 				{{svg "fontawesome-openid"}}
 				OpenID URI
@@ -21,14 +21,12 @@
 				<input id="openid" name="openid" value="{{.openid}}" autofocus required>
 			</div>
 			<div class="inline field">
-				<label></label>
 				<div class="ui checkbox">
 					<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
 					<input name="remember" type="checkbox">
 				</div>
 			</div>
 			<div class="inline field">
-				<label></label>
 				<button class="ui primary button">{{ctx.Locale.Tr "sign_in"}}</button>
 			</div>
 			</form>
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index e930bd3d15..bdb691d833 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -7,7 +7,7 @@
 		{{end}}
 	</h4>
 	<div class="ui attached segment">
-		<form class="ui form" action="{{.SignUpLink}}" method="post">
+		<form class="ui form tw-max-w-2xl tw-m-auto" action="{{.SignUpLink}}" method="post">
 			{{.CsrfTokenHtml}}
 			{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}}
 			{{template "base/alert" .}}
@@ -15,21 +15,21 @@
 			{{if .DisableRegistration}}
 				<p>{{ctx.Locale.Tr "auth.disable_register_prompt"}}</p>
 			{{else}}
-				<div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
+				<div class="required field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 					<label for="user_name">{{ctx.Locale.Tr "username"}}</label>
 					<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
 				</div>
-				<div class="required inline field {{if .Err_Email}}error{{end}}">
+				<div class="required field {{if .Err_Email}}error{{end}}">
 					<label for="email">{{ctx.Locale.Tr "email"}}</label>
 					<input id="email" name="email" type="email" value="{{.email}}" required>
 				</div>
 
 				{{if not .DisablePassword}}
-					<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
+					<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 						<label for="password">{{ctx.Locale.Tr "password"}}</label>
 						<input id="password" name="password" type="password" value="{{.password}}" autocomplete="new-password" required>
 					</div>
-					<div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
+					<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
 						<label for="retype">{{ctx.Locale.Tr "re_type"}}</label>
 						<input id="retype" name="retype" type="password" value="{{.retype}}" autocomplete="new-password" required>
 					</div>
@@ -38,7 +38,6 @@
 				{{template "user/auth/captcha" .}}
 
 				<div class="inline field">
-					<label></label>
 					<button class="ui primary button">
 						{{if .LinkAccountMode}}
 							{{ctx.Locale.Tr "auth.oauth_signup_submit"}}
@@ -50,7 +49,6 @@
 
 				{{if not .LinkAccountMode}}
 				<div class="inline field">
-					<label></label>
 					<a href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "auth.register_helper_msg"}}</a>
 				</div>
 				{{end}}
@@ -60,11 +58,11 @@
 			<div class="divider divider-text">
 				{{ctx.Locale.Tr "sign_in_or"}}
 			</div>
-			<div id="oauth2-login-navigator" class="gt-py-2">
-				<div class="gt-df gt-fc gt-jc">
-					<div id="oauth2-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3">
+			<div id="oauth2-login-navigator" class="tw-py-1">
+				<div class="tw-flex tw-flex-col tw-justify-center">
+					<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center tw-gap-2">
 						{{range $provider := .OAuth2Providers}}
-							<a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
+							<a class="{{$provider.Name}} ui button tw-flex tw-items-center tw-justify-center tw-py-2 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}">
 								{{$provider.IconHTML 28}}
 								{{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
 							</a>
diff --git a/templates/user/auth/signup_openid_navbar.tmpl b/templates/user/auth/signup_openid_navbar.tmpl
index 075f2e4d7b..89068ddde1 100644
--- a/templates/user/auth/signup_openid_navbar.tmpl
+++ b/templates/user/auth/signup_openid_navbar.tmpl
@@ -1,5 +1,5 @@
-<div class="ui secondary pointing tabular top attached borderless menu new-menu navbar">
-	<div class="new-menu-inner">
+<overflow-menu class="ui secondary pointing tabular top attached borderless menu secondary-nav">
+	<div class="overflow-menu-items tw-justify-center">
 		<a class="{{if .PageIsOpenIDConnect}}active {{end}}item" href="{{AppSubUrl}}/user/openid/connect">
 			{{ctx.Locale.Tr "auth.openid_connect_title"}}
 		</a>
@@ -9,4 +9,4 @@
 			</a>
 		{{end}}
 	</div>
-</div>
+</overflow-menu>
diff --git a/templates/user/auth/signup_openid_register.tmpl b/templates/user/auth/signup_openid_register.tmpl
index 81c36957d1..c017a0e65b 100644
--- a/templates/user/auth/signup_openid_register.tmpl
+++ b/templates/user/auth/signup_openid_register.tmpl
@@ -7,28 +7,27 @@
 					{{ctx.Locale.Tr "auth.openid_register_title"}}
 				</h4>
 				<div class="ui attached segment">
-					<p>
+					<p class="tw-max-w-2xl tw-mx-auto">
 						{{ctx.Locale.Tr "auth.openid_register_desc"}}
 					</p>
 					<form class="ui form" action="{{.Link}}" method="post">
 					{{.CsrfTokenHtml}}
-					<div class="required inline field {{if .Err_UserName}}error{{end}}">
+					<div class="required field {{if .Err_UserName}}error{{end}}">
 						<label for="user_name">{{ctx.Locale.Tr "username"}}</label>
 						<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
 					</div>
-					<div class="required inline field {{if .Err_Email}}error{{end}}">
+					<div class="required field {{if .Err_Email}}error{{end}}">
 						<label for="email">{{ctx.Locale.Tr "email"}}</label>
 						<input id="email" name="email" type="email" value="{{.email}}" required>
 					</div>
 
 					{{template "user/auth/captcha" .}}
 
-					<div class="inline field">
+					<div class="field">
 						<label for="openid">OpenID URI</label>
 						<input id="openid" value="{{.OpenID}}" readonly>
 					</div>
 					<div class="inline field">
-						<label></label>
 						<button class="ui primary button">{{ctx.Locale.Tr "auth.create_new_account"}}</button>
 					</div>
 					</form>
diff --git a/templates/user/auth/twofa.tmpl b/templates/user/auth/twofa.tmpl
index d325114155..d245239171 100644
--- a/templates/user/auth/twofa.tmpl
+++ b/templates/user/auth/twofa.tmpl
@@ -2,22 +2,21 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user signin">
 	<div class="ui middle very relaxed page grid">
 		<div class="column">
-			<form class="ui form" action="{{.Link}}" method="post">
+			<form class="ui form tw-max-w-2xl tw-m-auto" action="{{.Link}}" method="post">
 				{{.CsrfTokenHtml}}
 				<h3 class="ui top attached header">
 					{{ctx.Locale.Tr "twofa"}}
 				</h3>
 				<div class="ui attached segment">
 					{{template "base/alert" .}}
-					<div class="required inline field">
+					<div class="required field">
 						<label for="passcode">{{ctx.Locale.Tr "passcode"}}</label>
 						<input id="passcode" name="passcode" type="text" autocomplete="one-time-code" inputmode="numeric" pattern="[0-9]*" autofocus required>
 					</div>
 
 					<div class="inline field">
-						<label></label>
 						<button class="ui primary button">{{ctx.Locale.Tr "auth.verify"}}</button>
-						<a href="{{AppSubUrl}}/user/two_factor/scratch">{{ctx.Locale.Tr "auth.use_scratch_code" | Str2html}}</a>
+						<a href="{{AppSubUrl}}/user/two_factor/scratch">{{ctx.Locale.Tr "auth.use_scratch_code"}}</a>
 					</div>
 				</div>
 			</form>
diff --git a/templates/user/auth/twofa_scratch.tmpl b/templates/user/auth/twofa_scratch.tmpl
index 1aa044b4a5..23ad77f2a9 100644
--- a/templates/user/auth/twofa_scratch.tmpl
+++ b/templates/user/auth/twofa_scratch.tmpl
@@ -2,20 +2,19 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user signin">
 	<div class="ui middle very relaxed page grid">
 		<div class="column">
-			<form class="ui form" action="{{.Link}}" method="post">
+			<form class="ui form tw-max-w-2xl tw-m-auto" action="{{.Link}}" method="post">
 				{{.CsrfTokenHtml}}
 				<h3 class="ui top attached header">
 					{{ctx.Locale.Tr "twofa_scratch"}}
 				</h3>
 				<div class="ui attached segment">
 					{{template "base/alert" .}}
-					<div class="required inline field">
+					<div class="required field">
 						<label for="token">{{ctx.Locale.Tr "auth.scratch_code"}}</label>
 						<input id="token" name="token" type="text" autocomplete="off" autofocus required>
 					</div>
 
 					<div class="inline field">
-						<label></label>
 						<button class="ui primary button">{{ctx.Locale.Tr "auth.verify"}}</button>
 					</div>
 				</div>
diff --git a/templates/user/auth/webauthn.tmpl b/templates/user/auth/webauthn.tmpl
index 722da02f54..1b84765323 100644
--- a/templates/user/auth/webauthn.tmpl
+++ b/templates/user/auth/webauthn.tmpl
@@ -10,8 +10,8 @@
 				{{template "base/alert" .}}
 				<p>{{ctx.Locale.Tr "webauthn_sign_in"}}</p>
 			</div>
-			<div class="ui attached segment gt-df gt-ac gt-jc gt-gap-2 gt-py-3">
-				<div class="is-loading" style="width: 40px; height: 40px"></div>
+			<div class="ui attached segment tw-flex tw-items-center tw-justify-center tw-gap-1 tw-py-2">
+				<div class="is-loading tw-w-[40px] tw-h-[40px]"></div>
 				{{ctx.Locale.Tr "webauthn_press_button"}}
 			</div>
 			{{if .HasTwoFactor}}
diff --git a/templates/user/auth/webauthn_error.tmpl b/templates/user/auth/webauthn_error.tmpl
index fc6064db76..511ff7c287 100644
--- a/templates/user/auth/webauthn_error.tmpl
+++ b/templates/user/auth/webauthn_error.tmpl
@@ -1,7 +1,7 @@
-<div id="webauthn-error" class="ui negative message gt-hidden">
+<div id="webauthn-error" class="ui negative message tw-hidden">
 	<div class="header">{{ctx.Locale.Tr "webauthn_error"}}</div>
-	<div id="webauthn-error-msg" class="gt-pt-3"></div>
-	<div class="gt-hidden">
+	<div id="webauthn-error-msg" class="tw-pt-2"></div>
+	<div class="tw-hidden">
 		<div data-webauthn-error-msg="browser">{{ctx.Locale.Tr "webauthn_unsupported_browser"}}</div>
 		<div data-webauthn-error-msg="unknown">{{ctx.Locale.Tr "webauthn_error_unknown"}}</div>
 		<div data-webauthn-error-msg="insecure">{{ctx.Locale.Tr "webauthn_error_insecure"}}</div>
diff --git a/templates/user/code.tmpl b/templates/user/code.tmpl
index da9a3c3a24..ff6c69d615 100644
--- a/templates/user/code.tmpl
+++ b/templates/user/code.tmpl
@@ -1,10 +1,9 @@
 {{template "base/head" .}}
 {{if .ContextUser.IsOrganization}}
-	<div role="main" aria-label="{{.Title}}" class="page-content repository">
-		{{template "shared/user/org_profile_avatar" .}}
+	<div role="main" aria-label="{{.Title}}" class="page-content organization code">
+		{{template "org/header" .}}
 		<div class="ui container">
-			{{template "user/overview/header" .}}
-			{{template "code/searchcombo" .}}
+			{{template "shared/search/code/search" .}}
 		</div>
 	</div>
 {{else}}
@@ -16,7 +15,7 @@
 				</div>
 				<div class="ui twelve wide column">
 					{{template "user/overview/header" .}}
-					{{template "code/searchcombo" .}}
+					{{template "shared/search/code/search" .}}
 				</div>
 			</div>
 		</div>
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index a51365e4d6..60aa194534 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -4,79 +4,79 @@
 			<div class="flex-item-leading">
 				{{ctx.AvatarUtils.AvatarByAction .}}
 			</div>
-			<div class="flex-item-main gt-gap-3">
+			<div class="flex-item-main tw-gap-2">
 				<div>
 					{{if gt .ActUser.ID 0}}
-						<a href="{{AppSubUrl}}/{{(.GetActUserName ctx) | PathEscape}}" title="{{.GetDisplayNameTitle ctx}}">{{.GetDisplayName ctx}}</a>
+						<a href="{{AppSubUrl}}/{{(.GetActUserName ctx) | PathEscape}}" title="{{.GetActDisplayNameTitle ctx}}">{{.GetActDisplayName ctx}}</a>
 					{{else}}
 						{{.ShortActUserName ctx}}
 					{{end}}
 					{{if .GetOpType.InActions "create_repo"}}
-						{{ctx.Locale.Tr "action.create_repo" ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.create_repo" (.GetRepoLink ctx) (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "rename_repo"}}
-						{{ctx.Locale.Tr "action.rename_repo" (.GetContent|Escape) ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.rename_repo" .GetContent (.GetRepoLink ctx) (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "commit_repo"}}
 						{{if .Content}}
-							{{ctx.Locale.Tr "action.commit_repo" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (Escape .GetBranch) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+							{{ctx.Locale.Tr "action.commit_repo" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}}
 						{{else}}
-							{{ctx.Locale.Tr "action.create_branch" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (Escape .GetBranch) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+							{{ctx.Locale.Tr "action.create_branch" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}}
 						{{end}}
 					{{else if .GetOpType.InActions "create_issue"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.create_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.create_issue" (printf "%s/issues/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "create_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.create_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.create_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "transfer_repo"}}
-						{{ctx.Locale.Tr "action.transfer_repo" .GetContent ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.transfer_repo" .GetContent (.GetRepoLink ctx) (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "push_tag"}}
-						{{ctx.Locale.Tr "action.push_tag" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetTag|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.push_tag" (.GetRepoLink ctx) (.GetRefLink ctx) .GetTag (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "comment_issue"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.comment_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.comment_issue" (printf "%s/issues/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "merge_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.merge_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.merge_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "close_issue"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.close_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.close_issue" (printf "%s/issues/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "reopen_issue"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.reopen_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.reopen_issue" (printf "%s/issues/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "close_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.close_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.close_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "reopen_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.reopen_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.reopen_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "delete_tag"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.delete_tag" ((.GetRepoLink ctx)|Escape) (.GetTag|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.delete_tag" (.GetRepoLink ctx) .GetTag (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "delete_branch"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.delete_branch" ((.GetRepoLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.delete_branch" (.GetRepoLink ctx) .GetBranch (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "mirror_sync_push"}}
-						{{ctx.Locale.Tr "action.mirror_sync_push" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.mirror_sync_push" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "mirror_sync_create"}}
-						{{ctx.Locale.Tr "action.mirror_sync_create" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.mirror_sync_create" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "mirror_sync_delete"}}
-						{{ctx.Locale.Tr "action.mirror_sync_delete" ((.GetRepoLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.mirror_sync_delete" (.GetRepoLink ctx) .GetBranch (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "approve_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.approve_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.approve_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "reject_pull_request"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.reject_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.reject_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "comment_pull"}}
 						{{$index := index .GetIssueInfos 0}}
-						{{ctx.Locale.Tr "action.comment_pull" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}}
+						{{ctx.Locale.Tr "action.comment_pull" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{else if .GetOpType.InActions "publish_release"}}
 						{{$linkText := .Content | RenderEmoji $.Context}}
-						{{ctx.Locale.Tr "action.publish_release" ((.GetRepoLink ctx)|Escape) ((printf "%s/releases/tag/%s" (.GetRepoLink ctx) .GetTag)|Escape) ((.ShortRepoPath ctx)|Escape) $linkText | Str2html}}
+						{{ctx.Locale.Tr "action.publish_release" (.GetRepoLink ctx) (printf "%s/releases/tag/%s" (.GetRepoLink ctx) .GetTag) (.ShortRepoPath ctx) $linkText}}
 					{{else if .GetOpType.InActions "review_dismissed"}}
 						{{$index := index .GetIssueInfos 0}}
 						{{$reviewer := index .GetIssueInfos 1}}
-						{{ctx.Locale.Tr "action.review_dismissed" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) $reviewer | Str2html}}
+						{{ctx.Locale.Tr "action.review_dismissed" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx) $reviewer}}
 					{{end}}
 					{{TimeSince .GetCreate ctx.Locale}}
 				</div>
@@ -84,7 +84,7 @@
 					{{$push := ActionContent2Commits .}}
 					{{$repoLink := (.GetRepoLink ctx)}}
 					{{$repo := .Repo}}
-					<div class="gt-df gt-fc gt-gap-2">
+					<div class="tw-flex tw-flex-col tw-gap-1">
 						{{range $push.Commits}}
 							{{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}}
 							<div class="flex-text-block">
@@ -107,7 +107,7 @@
 					<a href="{{.GetCommentLink ctx}}" class="text truncate issue title">{{(.GetIssueTitle ctx) | RenderEmoji $.Context | RenderCodeBlock}}</a>
 					{{$comment := index .GetIssueInfos 1}}
 					{{if $comment}}
-						<div class="markup gt-font-14">{{RenderMarkdownToHtml ctx $comment}}</div>
+						<div class="markup tw-text-14">{{RenderMarkdownToHtml ctx $comment}}</div>
 					{{end}}
 				{{else if .GetOpType.InActions "merge_pull_request"}}
 					<div class="flex-item-body text black">{{index .GetIssueInfos 1}}</div>
@@ -119,7 +119,7 @@
 				{{end}}
 			</div>
 			<div class="flex-item-trailing">
-				{{svg (printf "octicon-%s" (ActionIcon .GetOpType)) 32 "text grey gt-mr-2"}}
+				{{svg (printf "octicon-%s" (ActionIcon .GetOpType)) 32 "text grey tw-mr-1"}}
 			</div>
 		</div>
 	{{end}}
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index 82622366e7..89f23163f7 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -2,32 +2,33 @@
 <div role="main" aria-label="{{.Title}}" class="page-content dashboard issues">
 	{{template "user/dashboard/navbar" .}}
 	<div class="ui container">
+		{{template "base/alert" .}}
 		<div class="flex-container">
 			<div class="flex-container-nav">
-				<div class="ui secondary vertical filter menu gt-bg-transparent">
-					<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{.Link}}?type=your_repositories&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
+				<div class="ui secondary vertical filter menu tw-bg-transparent">
+					<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="?type=your_repositories&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 						{{ctx.Locale.Tr "home.issues.in_your_repos"}}
 						<strong>{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
 					</a>
-					<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{.Link}}?type=assigned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
+					<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="?type=assigned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 						{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}
 						<strong>{{CountFmt .IssueStats.AssignCount}}</strong>
 					</a>
-					<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{.Link}}?type=created_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
+					<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="?type=created_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 						{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}
 						<strong>{{CountFmt .IssueStats.CreateCount}}</strong>
 					</a>
 					{{if .PageIsPulls}}
-						<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{.Link}}?type=review_requested&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
+						<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="?type=review_requested&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 							{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}
 							<strong>{{CountFmt .IssueStats.ReviewRequestedCount}}</strong>
 						</a>
-						<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{.Link}}?type=reviewed_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
+						<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="?type=reviewed_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 							{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
 							<strong>{{CountFmt .IssueStats.ReviewedCount}}</strong>
 						</a>
 					{{end}}
-					<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{.Link}}?type=mentioned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
+					<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="?type=mentioned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
 						{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}
 						<strong>{{CountFmt .IssueStats.MentionCount}}</strong>
 					</a>
@@ -36,12 +37,12 @@
 			<div class="flex-container-main content">
 				<div class="list-header">
 					<div class="small-menu-items ui compact tiny menu list-header-toggle">
-						<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
-							{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
+						<a class="item{{if not .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
+							{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
 							{{ctx.Locale.PrettyNumber .IssueStats.OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 						</a>
-						<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
-							{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
+						<a class="item{{if .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
+							{{svg "octicon-issue-closed" 16 "tw-mr-2"}}
 							{{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 						</a>
 					</div>
@@ -50,26 +51,26 @@
 							<input type="hidden" name="type" value="{{$.ViewType}}">
 							<input type="hidden" name="sort" value="{{$.SortType}}">
 							<input type="hidden" name="state" value="{{$.State}}">
-							{{template "shared/searchinput" dict "Value" $.Keyword}}
-							<button id="issue-list-quick-goto" class="ui small icon button gt-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}">{{svg "octicon-hash"}}</button>
-							<button class="ui small icon button" aria-label="{{ctx.Locale.Tr "explore.search"}}">{{svg "octicon-search"}}</button>
+							{{template "shared/search/input" dict "Value" $.Keyword}}
+							<button id="issue-list-quick-goto" class="ui small icon button tw-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}">{{svg "octicon-hash"}}</button>
+							{{template "shared/search/button"}}
 						</div>
 					</form>
 					<!-- Sort -->
 					<div class="list-header-sort ui small dropdown type jump item">
-						<span class="text gt-whitespace-nowrap">
+						<span class="text tw-whitespace-nowrap">
 							{{ctx.Locale.Tr "repo.issues.filter_sort"}}
 							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 						</span>
 						<div class="menu">
-							<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-							<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-							<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=latest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-							<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=oldest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-							<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
-							<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
-							<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
-							<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=farduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
+							<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+							<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+							<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="?type={{$.ViewType}}&sort=latest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
+							<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?type={{$.ViewType}}&sort=oldest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+							<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="?type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
+							<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="?type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
+							<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
+							<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=farduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
 						</div>
 					</div>
 				</div>
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index 390457a60a..0f1e866a21 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -4,7 +4,7 @@
 	<div class="ui container">
 		<div class="flex-container">
 			<div class="flex-container-nav">
-				<div class="ui secondary vertical filter menu gt-bg-transparent">
+				<div class="ui secondary vertical filter menu tw-bg-transparent">
 					<div class="item">
 						{{ctx.Locale.Tr "home.issues.in_your_repos"}}
 						<strong>{{.Total}}</strong>
@@ -12,7 +12,7 @@
 					<div class="divider"></div>
 					{{range .Repos}}
 						{{with $Repo := .}}
-							<a class="{{range $.RepoIDs}}{{if eq . $Repo.ID}}active{{end}}{{end}} repo name item" href="{{$.Link}}?repos=[
+							<a class="{{range $.RepoIDs}}{{if eq . $Repo.ID}}active{{end}}{{end}} repo name item" href="?repos=[
 								{{- with $include := true -}}
 										{{- range $.RepoIDs -}}
 											{{- if eq . $Repo.ID -}}
@@ -36,24 +36,21 @@
 			<div class="flex-container-main content">
 				<div class="list-header">
 					<div class="small-menu-items ui compact tiny menu list-header-toggle">
-						<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
-							{{svg "octicon-milestone" 16 "gt-mr-3"}}
+						<a class="item{{if not .IsShowClosed}} active{{end}}" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
+							{{svg "octicon-milestone" 16 "tw-mr-2"}}
 							{{ctx.Locale.PrettyNumber .MilestoneStats.OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 						</a>
-						<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
-							{{svg "octicon-check" 16 "gt-mr-3"}}
+						<a class="item{{if .IsShowClosed}} active{{end}}" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
+							{{svg "octicon-check" 16 "tw-mr-2"}}
 							{{ctx.Locale.PrettyNumber .MilestoneStats.ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 						</a>
 					</div>
 					<form class="list-header-search ui form ignore-dirty">
-						<div class="ui small search fluid action input">
-							<input type="hidden" name="type" value="{{$.ViewType}}">
+						<input type="hidden" name="type" value="{{$.ViewType}}">
 							<input type="hidden" name="repos" value="[{{range $.RepoIDs}}{{.}},{{end}}]">
 							<input type="hidden" name="sort" value="{{$.SortType}}">
 							<input type="hidden" name="state" value="{{$.State}}">
-							{{template "shared/searchinput" dict "Value" $.Keyword}}
-							<button class="ui small icon button" type="submit" aria-label="{{ctx.Locale.Tr "explore.search"}}">{{svg "octicon-search"}}</button>
-						</div>
+						{{template "shared/search/combo" dict "Value" $.Keyword}}
 					</form>
 					<!-- Sort -->
 					<div class="list-header-sort ui dropdown type jump item">
@@ -62,12 +59,12 @@
 						</span>
 						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 						<div class="menu">
-							<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.closest_due_date"}}</a>
-							<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.furthest_due_date"}}</a>
-							<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
-							<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
-							<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
-							<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
+							<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a>
+							<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a>
+							<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
+							<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
+							<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
+							<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
 						</div>
 					</div>
 				</div>
@@ -75,15 +72,15 @@
 					{{range .Milestones}}
 						<li class="milestone-card">
 							<div class="milestone-header">
-								<h3 class="flex-text-block gt-m-0">
+								<h3 class="flex-text-block tw-m-0">
 									<span class="ui large label">
 										{{.Repo.FullName}}
 									</span>
 									{{svg "octicon-milestone" 16}}
 									<a class="muted" href="{{.Repo.Link}}/milestone/{{.ID}}">{{.Name}}</a>
 								</h3>
-								<div class="gt-df gt-ac">
-									<span class="gt-mr-3">{{.Completeness}}%</span>
+								<div class="tw-flex tw-items-center">
+									<span class="tw-mr-2">{{.Completeness}}%</span>
 									<progress value="{{.Completeness}}" max="100"></progress>
 								</div>
 							</div>
@@ -106,14 +103,14 @@
 									{{if .UpdatedUnix}}
 										<div class="flex-text-block">
 											{{svg "octicon-clock"}}
-											{{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale) | Safe}}
+											{{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale)}}
 										</div>
 									{{end}}
 									<div class="flex-text-block">
 										{{if .IsClosed}}
 											{{$closedDate:= TimeSinceUnix .ClosedDateUnix ctx.Locale}}
 											{{svg "octicon-clock" 14}}
-											{{ctx.Locale.Tr "repo.milestones.closed" $closedDate | Safe}}
+											{{ctx.Locale.Tr "repo.milestones.closed" $closedDate}}
 										{{else}}
 											{{if .DeadlineString}}
 												<span{{if .IsOverdue}} class="text red"{{end}}>
@@ -141,7 +138,7 @@
 							</div>
 							{{if .Content}}
 								<div class="markup content">
-									{{.RenderedContent|Str2html}}
+									{{.RenderedContent}}
 								</div>
 							{{end}}
 						</li>
diff --git a/templates/user/dashboard/navbar.tmpl b/templates/user/dashboard/navbar.tmpl
index af07897e2c..464228289e 100644
--- a/templates/user/dashboard/navbar.tmpl
+++ b/templates/user/dashboard/navbar.tmpl
@@ -1,15 +1,15 @@
-<div class="dashboard-navbar">
+<div class="secondary-nav tw-border-b tw-border-b-secondary">
 	<div class="ui secondary stackable menu">
 		<div class="item">
 			<div class="ui floating dropdown jump">
 				<span class="text truncated-item-container">
-					{{ctx.AvatarUtils.Avatar .ContextUser}}
+					{{ctx.AvatarUtils.Avatar .ContextUser 24 "tw-mr-1"}}
 					<span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span>
 					<span class="org-visibility">
 						{{if .ContextUser.Visibility.IsLimited}}<div class="ui basic tiny horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</div>{{end}}
 						{{if .ContextUser.Visibility.IsPrivate}}<div class="ui basic tiny horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</div>{{end}}
 					</span>
-					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+					{{svg "octicon-triangle-down" 14 "dropdown icon tw-ml-1"}}
 				</span>
 				<div class="context user overflow menu">
 					<div class="ui header">
@@ -78,7 +78,7 @@
 
 	{{if .ContextUser.IsOrganization}}
 		<div class="right menu">
-			<a class="{{if .PageIsNews}}active {{end}}item gt-ml-auto" href="{{.ContextUser.DashboardLink}}{{if .Team}}/{{PathEscape .Team.Name}}{{end}}">
+			<a class="{{if .PageIsNews}}active {{end}}item tw-ml-auto" href="{{.ContextUser.DashboardLink}}{{if .Team}}/{{PathEscape .Team.Name}}{{end}}">
 				{{svg "octicon-rss"}}&nbsp;{{ctx.Locale.Tr "activities"}}
 			</a>
 			{{if not .UnitIssuesGlobalDisabled}}
@@ -105,4 +105,3 @@
 	{{end}}
 	</div>
 </div>
-<div class="divider"></div>
diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl
index ed6f906822..a879f1fb9d 100644
--- a/templates/user/dashboard/repolist.tmpl
+++ b/templates/user/dashboard/repolist.tmpl
@@ -9,7 +9,7 @@ const data = {
 	textOrganization: {{ctx.Locale.Tr "organization"}},
 	textMyRepos: {{ctx.Locale.Tr "home.my_repos"}},
 	textNewRepo: {{ctx.Locale.Tr "new_repo"}},
-	textSearchRepos: {{ctx.Locale.Tr "home.search_repos"}},
+	textSearchRepos: {{ctx.Locale.Tr "search.repo_kind"}},
 	textFilter: {{ctx.Locale.Tr "home.filter"}},
 	textShowArchived: {{ctx.Locale.Tr "home.show_archived"}},
 	textShowPrivate: {{ctx.Locale.Tr "home.show_private"}},
diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl
index d8f8d462d3..bf3b51ee3b 100644
--- a/templates/user/notification/notification_div.tmpl
+++ b/templates/user/notification/notification_div.tmpl
@@ -1,11 +1,11 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user notification" id="notification_div" data-sequence-number="{{.SequenceNumber}}">
 	<div class="ui container">
 		{{$notificationUnreadCount := call .NotificationUnreadCount}}
-		<div class="gt-df gt-ac gt-sb gt-mb-4">
+		<div class="tw-flex tw-items-center tw-justify-between tw-mb-[--page-spacing]">
 			<div class="small-menu-items ui compact tiny menu">
 				<a class="{{if eq .Status 1}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=unread">
 					{{ctx.Locale.Tr "notification.unread"}}
-					<div class="notifications-unread-count ui label {{if not $notificationUnreadCount}}gt-hidden{{end}}">{{$notificationUnreadCount}}</div>
+					<div class="notifications-unread-count ui label {{if not $notificationUnreadCount}}tw-hidden{{end}}">{{$notificationUnreadCount}}</div>
 				</a>
 				<a class="{{if eq .Status 2}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=read">
 					{{ctx.Locale.Tr "notification.read"}}
@@ -14,19 +14,19 @@
 			{{if and (eq .Status 1)}}
 				<form action="{{AppSubUrl}}/notifications/purge" method="post">
 					{{$.CsrfTokenHtml}}
-					<div class="{{if not $notificationUnreadCount}}gt-hidden{{end}}">
-						<button class="ui mini button primary gt-mr-0" title="{{ctx.Locale.Tr "notification.mark_all_as_read"}}">
+					<div class="{{if not $notificationUnreadCount}}tw-hidden{{end}}">
+						<button class="ui mini button primary tw-mr-0" title="{{ctx.Locale.Tr "notification.mark_all_as_read"}}">
 							{{svg "octicon-checklist"}}
 						</button>
 					</div>
 				</form>
 			{{end}}
 		</div>
-		<div class="gt-p-0">
+		<div class="tw-p-0">
 			<div id="notification_table">
 				{{if not .Notifications}}
-					<div class="gt-df gt-ac gt-fc gt-p-4">
-						{{svg "octicon-inbox" 56 "gt-mb-4"}}
+					<div class="tw-flex tw-items-center tw-flex-col tw-p-4">
+						{{svg "octicon-inbox" 56 "tw-mb-4"}}
 						{{if eq .Status 1}}
 							{{ctx.Locale.Tr "notification.no_unread"}}
 						{{else}}
@@ -35,22 +35,22 @@
 					</div>
 				{{else}}
 					{{range $notification := .Notifications}}
-						<div class="notifications-item gt-df gt-ac gt-fw gt-gap-3 gt-p-3" id="notification_{{.ID}}" data-status="{{.Status}}">
-							<div class="notifications-icon gt-ml-3 gt-mr-2 gt-self-start gt-mt-2">
+						<div class="notifications-item tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-p-2" id="notification_{{.ID}}" data-status="{{.Status}}">
+							<div class="notifications-icon tw-ml-2 tw-mr-1 tw-self-start tw-mt-1">
 								{{if .Issue}}
 									{{template "shared/issueicon" .Issue}}
 								{{else}}
 									{{svg "octicon-repo" 16 "text grey"}}
 								{{end}}
 							</div>
-							<a class="notifications-link gt-df gt-f1 gt-fc silenced" href="{{.Link ctx}}">
-								<div class="notifications-top-row gt-font-13">
+							<a class="notifications-link tw-flex tw-flex-1 tw-flex-col silenced" href="{{.Link ctx}}">
+								<div class="notifications-top-row tw-text-13">
 									{{.Repository.FullName}} {{if .Issue}}<span class="text light-3">#{{.Issue.Index}}</span>{{end}}
 									{{if eq .Status 3}}
-										{{svg "octicon-pin" 13 "text blue gt-mt-1 gt-ml-2"}}
+										{{svg "octicon-pin" 13 "text blue tw-mt-0.5 tw-ml-1"}}
 									{{end}}
 								</div>
-								<div class="notifications-bottom-row gt-font-16 gt-py-1">
+								<div class="notifications-bottom-row tw-text-16 tw-py-0.5">
 									<span class="issue-title">
 										{{if .Issue}}
 											{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}
@@ -60,20 +60,20 @@
 									</span>
 								</div>
 							</a>
-							<div class="notifications-updated gt-ac gt-mr-3">
+							<div class="notifications-updated tw-items-center tw-mr-2">
 								{{if .Issue}}
 									{{TimeSinceUnix .Issue.UpdatedUnix ctx.Locale}}
 								{{else}}
 									{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
 								{{end}}
 							</div>
-							<div class="notifications-buttons gt-ac gt-je gt-gap-2 gt-px-2">
+							<div class="notifications-buttons tw-items-center tw-justify-end tw-gap-1 tw-px-1">
 								{{if ne .Status 3}}
 									<form action="{{AppSubUrl}}/notifications/status" method="post">
 										{{$.CsrfTokenHtml}}
 										<input type="hidden" name="notification_id" value="{{.ID}}">
 										<input type="hidden" name="status" value="pinned">
-										<button class="btn interact-bg gt-p-3" title="{{ctx.Locale.Tr "notification.pin"}}"
+										<button class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.pin"}}"
 											data-url="{{AppSubUrl}}/notifications/status"
 											data-status="pinned"
 											data-page="{{$.Page.Paginater.Current}}"
@@ -89,7 +89,7 @@
 										<input type="hidden" name="notification_id" value="{{.ID}}">
 										<input type="hidden" name="status" value="read">
 										<input type="hidden" name="page" value="{{$.Page.Paginater.Current}}">
-										<button class="btn interact-bg gt-p-3" title="{{ctx.Locale.Tr "notification.mark_as_read"}}"
+										<button class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.mark_as_read"}}"
 											data-url="{{AppSubUrl}}/notifications/status"
 											data-status="read"
 											data-page="{{$.Page.Paginater.Current}}"
@@ -104,7 +104,7 @@
 										<input type="hidden" name="notification_id" value="{{.ID}}">
 										<input type="hidden" name="status" value="unread">
 										<input type="hidden" name="page" value="{{$.Page.Paginater.Current}}">
-										<button class="btn interact-bg gt-p-3" title="{{ctx.Locale.Tr "notification.mark_as_unread"}}"
+										<button class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.mark_as_unread"}}"
 											data-url="{{AppSubUrl}}/notifications/status"
 											data-status="unread"
 											data-page="{{$.Page.Paginater.Current}}"
diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl
index ec40d3afea..a5a965ca52 100644
--- a/templates/user/notification/notification_subscriptions.tmpl
+++ b/templates/user/notification/notification_subscriptions.tmpl
@@ -11,23 +11,23 @@
 		</div>
 		<div class="ui bottom attached active tab segment">
 			{{if eq .Status 1}}
-				<div class="gt-df gt-sb">
-					<div class="gt-df">
+				<div class="tw-flex tw-justify-between">
+					<div class="tw-flex">
 						<div class="small-menu-items ui compact tiny menu">
-							<a class="{{if eq .State "all"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=all&issueType={{$.IssueType}}&labels={{$.Labels}}">
+							<a class="{{if eq .State "all"}}active {{end}}item" href="?sort={{$.SortType}}&state=all&issueType={{$.IssueType}}&labels={{$.Labels}}">
 								{{ctx.Locale.Tr "all"}}
 							</a>
-							<a class="{{if eq .State "open"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=open&issueType={{$.IssueType}}&labels={{$.Labels}}">
-								{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
+							<a class="{{if eq .State "open"}}active {{end}}item" href="?sort={{$.SortType}}&state=open&issueType={{$.IssueType}}&labels={{$.Labels}}">
+								{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
 								{{ctx.Locale.Tr "repo.issues.open_title"}}
 							</a>
-							<a class="{{if eq .State "closed"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=closed&issueType={{$.IssueType}}&labels={{$.Labels}}">
-								{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
+							<a class="{{if eq .State "closed"}}active {{end}}item" href="?sort={{$.SortType}}&state=closed&issueType={{$.IssueType}}&labels={{$.Labels}}">
+								{{svg "octicon-issue-closed" 16 "tw-mr-2"}}
 								{{ctx.Locale.Tr "repo.issues.closed_title"}}
 							</a>
 						</div>
 					</div>
-					<div class="gt-df gt-sb">
+					<div class="tw-flex tw-justify-between">
 						<div class="ui right aligned secondary filter menu labels">
 							<!-- Type -->
 								<div class="ui dropdown type jump item">
@@ -36,9 +36,9 @@
 									</span>
 									{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 									<div class="menu">
-										<a class="{{if or (eq .IssueType "all") (not .IssueType)}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=all&labels={{$.Labels}}">{{ctx.Locale.Tr "all"}}</a>
-										<a class="{{if eq .IssueType "issues"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=issues&labels={{$.Labels}}">{{ctx.Locale.Tr "issues"}}</a>
-										<a class="{{if eq .IssueType "pulls"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=pulls&labels={{$.Labels}}">{{ctx.Locale.Tr "pull_requests"}}</a>
+										<a class="{{if or (eq .IssueType "all") (not .IssueType)}}active {{end}}item" href="?sort={{$.SortType}}&state={{$.State}}&issueType=all&labels={{$.Labels}}">{{ctx.Locale.Tr "all"}}</a>
+										<a class="{{if eq .IssueType "issues"}}active {{end}}item" href="?sort={{$.SortType}}&state={{$.State}}&issueType=issues&labels={{$.Labels}}">{{ctx.Locale.Tr "issues"}}</a>
+										<a class="{{if eq .IssueType "pulls"}}active {{end}}item" href="?sort={{$.SortType}}&state={{$.State}}&issueType=pulls&labels={{$.Labels}}">{{ctx.Locale.Tr "pull_requests"}}</a>
 									</div>
 								</div>
 
@@ -49,14 +49,14 @@
 								</span>
 								{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 								<div class="menu">
-									<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=latest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
-									<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?sort=oldest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
-									<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-									<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-									<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{$.Link}}?sort=mostcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
-									<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{$.Link}}?sort=leastcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
-									<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{$.Link}}?sort=nearduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
-									<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{$.Link}}?sort=farduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
+									<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="?sort=latest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
+									<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?sort=oldest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+									<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?sort=recentupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+									<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?sort=leastupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+									<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="?sort=mostcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
+									<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="?sort=leastcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
+									<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="?sort=nearduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
+									<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="?sort=farduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
 								</div>
 							</div>
 						</div>
@@ -69,7 +69,7 @@
 					{{template "shared/issuelist" dict "." . "listType" "dashboard"}}
 				{{end}}
 			{{else}}
-				{{template "explore/repo_search" .}}
+				{{template "shared/repo_search" .}}
 				{{template "explore/repo_list" .}}
 				{{template "base/paginate" .}}
 			{{end}}
diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl
index c0cbe2561c..275c4e295e 100644
--- a/templates/user/overview/header.tmpl
+++ b/templates/user/overview/header.tmpl
@@ -1,72 +1,50 @@
-<div class="ui secondary stackable pointing menu">
-	{{if and .HasProfileReadme .ContextUser.IsIndividual}}
-	<a class="{{if eq .TabName "overview"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=overview">
-		{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
-	</a>
-	{{end}}
-	<a class="{{if eq .TabName "repositories"}}active {{end}} item" href="{{.ContextUser.HomeLink}}?tab=repositories">
-		{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
-		{{if .RepoCount}}
-			<div class="ui small label">{{.RepoCount}}</div>
-		{{end}}
-	</a>
-	{{if or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadProjects)}}
-	<a href="{{.ContextUser.HomeLink}}/-/projects" class="{{if .PageIsViewProjects}}active {{end}}item">
-		{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}}
-		{{if .ProjectCount}}
-			<div class="ui small label">{{.ProjectCount}}</div>
-		{{end}}
-	</a>
-	{{end}}
-	{{if and .IsPackageEnabled (or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadPackages))}}
-		<a href="{{.ContextUser.HomeLink}}/-/packages" class="{{if .IsPackagesPage}}active {{end}}item">
-			{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
+<overflow-menu class="ui secondary pointing tabular borderless menu">
+	<div class="overflow-menu-items">
+		{{if and .HasProfileReadme .ContextUser.IsIndividual}}
+		<a class="{{if eq .TabName "overview"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=overview">
+			{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
 		</a>
-	{{end}}
-	{{if and .IsRepoIndexerEnabled (or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadCode))}}
-		<a href="{{.ContextUser.HomeLink}}/-/code" class="{{if .IsCodePage}}active {{end}}item">
-			{{svg "octicon-code"}} {{ctx.Locale.Tr "user.code"}}
+		{{end}}
+		<a class="{{if eq .TabName "repositories"}}active {{end}} item" href="{{.ContextUser.HomeLink}}?tab=repositories">
+			{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
+			{{if .RepoCount}}
+				<div class="ui small label">{{.RepoCount}}</div>
+			{{end}}
 		</a>
-	{{end}}
-
-	{{if .ContextUser.IsOrganization}}
-		{{if .NumMembers}}
-			<a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members">
-				{{svg "octicon-person"}}&nbsp;{{ctx.Locale.Tr "org.members"}}
-				<div class="ui small label">{{.NumMembers}}</div>
+		{{if or .ContextUser.IsIndividual .CanReadProjects}}
+		<a href="{{.ContextUser.HomeLink}}/-/projects" class="{{if .PageIsViewProjects}}active {{end}}item">
+			{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}}
+			{{if .ProjectCount}}
+				<div class="ui small label">{{.ProjectCount}}</div>
+			{{end}}
+		</a>
+		{{end}}
+		{{if and .IsPackageEnabled (or .ContextUser.IsIndividual .CanReadPackages)}}
+			<a href="{{.ContextUser.HomeLink}}/-/packages" class="{{if .IsPackagesPage}}active {{end}}item">
+				{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
 			</a>
 		{{end}}
-		{{if .IsOrganizationMember}}
-			<a class="{{if $.PageIsOrgTeams}}active {{end}}item" href="{{$.OrgLink}}/teams">
-				{{svg "octicon-people"}}&nbsp;{{ctx.Locale.Tr "org.teams"}}
-				{{if .NumTeams}}
-					<div class="ui small label">{{.NumTeams}}</div>
-				{{end}}
+		{{if and .IsRepoIndexerEnabled (or .ContextUser.IsIndividual .CanReadCode)}}
+			<a href="{{.ContextUser.HomeLink}}/-/code" class="{{if .IsCodePage}}active {{end}}item">
+				{{svg "octicon-code"}} {{ctx.Locale.Tr "user.code"}}
 			</a>
 		{{end}}
-
-		{{if .IsOrganizationOwner}}
-			<div class="right menu">
-				<a class="item" href="{{.OrgLink}}/settings">
-				{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
-				</a>
-			</div>
-		{{end}}
-	{{else}}
-		<a class="{{if eq .TabName "activity"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=activity">
-			{{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}}
-		</a>
-		{{if not .DisableStars}}
+		{{if .ContextUser.IsIndividual}}
+			<a class="{{if eq .TabName "activity"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=activity">
+				{{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}}
+			</a>
+			{{if not .DisableStars}}
 			<a class="{{if eq .TabName "stars"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=stars">
 				{{svg "octicon-star"}} {{ctx.Locale.Tr "user.starred"}}
 				{{if .ContextUser.NumStars}}
 					<div class="ui small label">{{.ContextUser.NumStars}}</div>
 				{{end}}
 			</a>
-		{{else}}
+			{{else}}
 			<a class="{{if eq .TabName "watching"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=watching">
 				{{svg "octicon-eye"}} {{ctx.Locale.Tr "user.watched"}}
 			</a>
+			{{end}}
 		{{end}}
-	{{end}}
-</div>
+	</div>
+</overflow-menu>
diff --git a/templates/user/overview/package_versions.tmpl b/templates/user/overview/package_versions.tmpl
index 6f740e0e7c..0ac2db0d86 100644
--- a/templates/user/overview/package_versions.tmpl
+++ b/templates/user/overview/package_versions.tmpl
@@ -1,10 +1,9 @@
 {{template "base/head" .}}
 {{if .ContextUser.IsOrganization}}
-	<div role="main" aria-label="{{.Title}}" class="page-content repository packages">
-		{{template "shared/user/org_profile_avatar" .}}
+	<div role="main" aria-label="{{.Title}}" class="page-content organization packages">
+		{{template "org/header" .}}
 		<div class="ui container">
-		{{template "user/overview/header" .}}
-		{{template "package/shared/versionlist" .}}
+			{{template "package/shared/versionlist" .}}
 		</div>
 	</div>
 {{else}}
@@ -14,11 +13,9 @@
 				<div class="ui four wide column">
 					{{template "shared/user/profile_big_avatar" .}}
 				</div>
-				<div class="ui twelve wide column">
-					<div class="gt-mb-4">
+				<div class="ui twelve wide column tw-mb-4">
 						{{template "user/overview/header" .}}
-					</div>
-					{{template "package/shared/versionlist" .}}
+						{{template "package/shared/versionlist" .}}
 				</div>
 			</div>
 		</div>
diff --git a/templates/user/overview/packages.tmpl b/templates/user/overview/packages.tmpl
index 4fd17696d1..bb2238b919 100644
--- a/templates/user/overview/packages.tmpl
+++ b/templates/user/overview/packages.tmpl
@@ -1,10 +1,9 @@
 {{template "base/head" .}}
 {{if .ContextUser.IsOrganization}}
-	<div role="main" aria-label="{{.Title}}" class="page-content repository packages">
-		{{template "shared/user/org_profile_avatar" .}}
+	<div role="main" aria-label="{{.Title}}" class="page-content organization packages">
+		{{template "org/header" .}}
 		<div class="ui container">
-		{{template "user/overview/header" .}}
-		{{template "package/shared/list" .}}
+			{{template "package/shared/list" .}}
 		</div>
 	</div>
 {{else}}
@@ -14,11 +13,9 @@
 				<div class="ui four wide column">
 					{{template "shared/user/profile_big_avatar" .}}
 				</div>
-				<div class="ui twelve wide column">
-					<div class="gt-mb-4">
+				<div class="ui twelve wide column tw-mb-4">
 						{{template "user/overview/header" .}}
-					</div>
-					{{template "package/shared/list" .}}
+						{{template "package/shared/list" .}}
 				</div>
 			</div>
 		</div>
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl
index 426b5f042a..cf61bb906a 100644
--- a/templates/user/profile.tmpl
+++ b/templates/user/profile.tmpl
@@ -5,11 +5,8 @@
 			<div class="ui four wide column">
 				{{template "shared/user/profile_big_avatar" .}}
 			</div>
-			<div class="ui twelve wide column">
-				<div class="gt-mb-4">
-					{{template "user/overview/header" .}}
-				</div>
-
+			<div class="ui twelve wide column tw-mb-4">
+				{{template "user/overview/header" .}}
 				{{if eq .TabName "activity"}}
 					{{if .ContextUser.KeepActivityPrivate}}
 						<div class="ui info message">
@@ -20,7 +17,7 @@
 					{{template "user/dashboard/feeds" .}}
 				{{else if eq .TabName "stars"}}
 					<div class="stars">
-						{{template "explore/repo_search" .}}
+						{{template "shared/repo_search" .}}
 						{{template "explore/repo_list" .}}
 						{{template "base/paginate" .}}
 					</div>
@@ -29,9 +26,9 @@
 				{{else if eq .TabName "followers"}}
 					{{template "repo/user_cards" .}}
 				{{else if eq .TabName "overview"}}
-					<div id="readme_profile" class="markup">{{.ProfileReadme | Str2html}}</div>
+					<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
 				{{else}}
-					{{template "explore/repo_search" .}}
+					{{template "shared/repo_search" .}}
 					{{template "explore/repo_list" .}}
 					{{template "base/paginate" .}}
 				{{end}}
diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl
index 7c6fd49a08..040f46e48b 100644
--- a/templates/user/settings/account.tmpl
+++ b/templates/user/settings/account.tmpl
@@ -42,11 +42,11 @@
 			<div class="ui list">
 				{{if $.EnableNotifyMail}}
 				<div class="item">
-					<div class="gt-mb-3">{{ctx.Locale.Tr "settings.email_desc"}}</div>
+					<div class="tw-mb-2">{{ctx.Locale.Tr "settings.email_desc"}}</div>
 					<form action="{{AppSubUrl}}/user/settings/account/email" class="ui form" method="post">
 						{{$.CsrfTokenHtml}}
 						<input name="_method" type="hidden" value="NOTIFICATION">
-						<div class="gt-df gt-fw gt-gap-3">
+						<div class="tw-flex tw-flex-wrap tw-gap-2">
 							<div class="ui selection dropdown">
 								<input name="preference" type="hidden" value="{{.EmailNotificationsPreference}}">
 								{{svg "octicon-triangle-down" 14 "dropdown icon"}}
@@ -96,7 +96,7 @@
 								</form>
 							</div>
 						{{end}}
-						<div class="content gt-py-3">
+						<div class="content tw-py-2">
 							<strong>{{.Email}}</strong>
 							{{if .IsPrimary}}
 								<div class="ui primary label">{{ctx.Locale.Tr "settings.primary"}}</div>
@@ -128,14 +128,15 @@
 			{{end}}
 		</div>
 
+		{{if not ($.UserDisabledFeatures.Contains "deletion")}}
 		<h4 class="ui top attached error header">
 			{{ctx.Locale.Tr "settings.delete_account"}}
 		</h4>
 		<div class="ui attached error segment">
 			<div class="ui red message">
-				<p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "settings.delete_prompt" | Str2html}}</p>
+				<p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "settings.delete_prompt"}}</p>
 				{{if .UserDeleteWithComments}}
-				<p class="text left gt-font-semibold">{{ctx.Locale.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxTime | Str2html}}</p>
+				<p class="text left tw-font-semibold">{{ctx.Locale.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxTime}}</p>
 				{{end}}
 			</div>
 			<form class="ui form ignore-dirty" id="delete-form" action="{{AppSubUrl}}/user/settings/account/delete" method="post">
@@ -151,7 +152,18 @@
 					</button>
 				</div>
 			</form>
+			<div class="ui g-modal-confirm delete modal" id="delete-account">
+				<div class="header">
+					{{svg "octicon-trash"}}
+					{{ctx.Locale.Tr "settings.delete_account_title"}}
+				</div>
+				<div class="content">
+					<p>{{ctx.Locale.Tr "settings.delete_account_desc"}}</p>
+				</div>
+				{{template "base/modal_actions_confirm" .}}
+			</div>
 		</div>
+		{{end}}
 	</div>
 
 <div class="ui g-modal-confirm delete modal" id="delete-email">
@@ -165,15 +177,4 @@
 	{{template "base/modal_actions_confirm" .}}
 </div>
 
-<div class="ui g-modal-confirm delete modal" id="delete-account">
-	<div class="header">
-		{{svg "octicon-trash"}}
-		{{ctx.Locale.Tr "settings.delete_account_title"}}
-	</div>
-	<div class="content">
-		<p>{{ctx.Locale.Tr "settings.delete_account_desc"}}</p>
-	</div>
-	{{template "base/modal_actions_confirm" .}}
-</div>
-
 {{template "user/settings/layout_footer" .}}
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index 7553c798dc..5e2ffc3bb3 100644
--- a/templates/user/settings/applications.tmpl
+++ b/templates/user/settings/applications.tmpl
@@ -18,7 +18,7 @@
 						<div class="flex-item-main">
 							<details>
 								<summary><span class="flex-item-title">{{.Name}}</span></summary>
-								<p class="gt-my-2">
+								<p class="tw-my-1">
 									{{ctx.Locale.Tr "settings.repo_and_org_access"}}:
 									{{if .DisplayPublicOnly}}
 										{{ctx.Locale.Tr "settings.permissions_public_only"}}
@@ -26,8 +26,8 @@
 										{{ctx.Locale.Tr "settings.permissions_access_all"}}
 									{{end}}
 								</p>
-								<p class="gt-my-2">{{ctx.Locale.Tr "settings.permissions_list"}}</p>
-								<ul class="gt-my-2">
+								<p class="tw-my-1">{{ctx.Locale.Tr "settings.permissions_list"}}</p>
+								<ul class="tw-my-1">
 								{{range .Scope.StringSlice}}
 									{{if (ne . $.AccessTokenScopePublicOnly)}}
 										<li>{{.}}</li>
@@ -36,12 +36,12 @@
 								</ul>
 							</details>
 							<div class="flex-item-body">
-								<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} — {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
+								<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} — {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
 							</div>
 						</div>
 						<div class="flex-item-trailing">
 								<button class="ui red tiny button delete-button" data-modal-id="delete-token" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
-									{{svg "octicon-trash" 16 "gt-mr-2"}}
+									{{svg "octicon-trash" 16 "tw-mr-1"}}
 									{{ctx.Locale.Tr "settings.delete_token"}}
 								</button>
 						</div>
@@ -61,21 +61,21 @@
 				</div>
 				<div class="field">
 					<label>{{ctx.Locale.Tr "settings.repo_and_org_access"}}</label>
-					<label class="gt-cursor-pointer">
-						<input class="enable-system gt-mt-2 gt-mr-2" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}">
+					<label class="tw-cursor-pointer">
+						<input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}">
 						{{ctx.Locale.Tr "settings.permissions_public_only"}}
 					</label>
-					<label class="gt-cursor-pointer">
-						<input class="enable-system gt-mt-2 gt-mr-2" type="radio" name="scope" value="" checked>
+					<label class="tw-cursor-pointer">
+						<input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="" checked>
 						{{ctx.Locale.Tr "settings.permissions_access_all"}}
 					</label>
 				</div>
 				<details class="ui optional field">
-					<summary class="gt-pb-4 gt-pl-2">
+					<summary class="tw-pb-4 tw-pl-1">
 						{{ctx.Locale.Tr "settings.select_permissions"}}
 					</summary>
 					<p class="activity meta">
-						<i>{{ctx.Locale.Tr "settings.access_token_desc" (printf `href="/api/swagger" target="_blank"`) (printf `href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`) | Str2html}}</i>
+						<i>{{ctx.Locale.Tr "settings.access_token_desc" (`href="/api/swagger" target="_blank"`|SafeHTML) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</i>
 					</p>
 					<div class="scoped-access-token-mount">
 						<scoped-access-token-selector
@@ -90,7 +90,7 @@
 					{{ctx.Locale.Tr "settings.generate_token"}}
 				</button>
 			</form>{{/* Fomantic ".ui.form .warning.message" is hidden by default, so put the warning message out of the form*/}}
-			<div id="scoped-access-warning" class="ui warning message center gt-hidden">
+			<div id="scoped-access-warning" class="ui warning message center tw-hidden">
 				{{ctx.Locale.Tr "settings.at_least_one_permission"}}
 			</div>
 		</div>
diff --git a/templates/user/settings/applications_oauth2_edit_form.tmpl b/templates/user/settings/applications_oauth2_edit_form.tmpl
index c0bddd55b3..f7ef115693 100644
--- a/templates/user/settings/applications_oauth2_edit_form.tmpl
+++ b/templates/user/settings/applications_oauth2_edit_form.tmpl
@@ -26,7 +26,7 @@
 		<form class="ui form ignore-dirty" action="{{.FormActionPath}}/regenerate_secret" method="post">
 			{{.CsrfTokenHtml}}
 			{{ctx.Locale.Tr "settings.oauth2_regenerate_secret_hint"}}
-			<button class="ui mini button gt-ml-3" type="submit">{{ctx.Locale.Tr "settings.oauth2_regenerate_secret"}}</button>
+			<button class="ui mini button tw-ml-2" type="submit">{{ctx.Locale.Tr "settings.oauth2_regenerate_secret"}}</button>
 		</form>
 	</div>
 </div>
diff --git a/templates/user/settings/applications_oauth2_list.tmpl b/templates/user/settings/applications_oauth2_list.tmpl
index bf6b28ec5f..cfcb6d053d 100644
--- a/templates/user/settings/applications_oauth2_list.tmpl
+++ b/templates/user/settings/applications_oauth2_list.tmpl
@@ -4,7 +4,7 @@
 			{{ctx.Locale.Tr "settings.oauth2_application_create_description"}}
 		</div>
 		{{range .Applications}}
-			<div class="flex-item gt-ac">
+			<div class="flex-item tw-items-center">
 				<div class="flex-item-leading">
 					{{svg "octicon-apps" 32}}
 				</div>
@@ -21,12 +21,12 @@
 						<span class="ui basic label" data-tooltip-content="{{ctx.Locale.Tr "settings.oauth2_application_locked"}}">{{ctx.Locale.Tr "locked"}}</span>
 					{{else}}
 						<a href="{{$.Link}}/oauth2/{{.ID}}" class="ui primary tiny button">
-							{{svg "octicon-pencil" 16 "gt-mr-2"}}
+							{{svg "octicon-pencil" 16 "tw-mr-1"}}
 							{{ctx.Locale.Tr "settings.oauth2_application_edit"}}
 						</a>
 						<button class="ui red tiny button delete-button" data-modal-id="remove-gitea-oauth2-application"
 								data-url="{{$.Link}}/oauth2/{{.ID}}/delete">
-							{{svg "octicon-trash" 16 "gt-mr-2"}}
+							{{svg "octicon-trash" 16 "tw-mr-1"}}
 							{{ctx.Locale.Tr "settings.delete_key"}}
 						</button>
 					{{end}}
diff --git a/templates/user/settings/blocked_users.tmpl b/templates/user/settings/blocked_users.tmpl
new file mode 100644
index 0000000000..e495b85f58
--- /dev/null
+++ b/templates/user/settings/blocked_users.tmpl
@@ -0,0 +1,5 @@
+{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings blocked_users")}}
+	<div class="user-setting-content">
+		{{template "shared/user/blocked_users" .}}
+	</div>
+{{template "user/settings/layout_footer" .}}
diff --git a/templates/user/settings/grants_oauth2.tmpl b/templates/user/settings/grants_oauth2.tmpl
index 3c4c6e80d4..b5ae3e0337 100644
--- a/templates/user/settings/grants_oauth2.tmpl
+++ b/templates/user/settings/grants_oauth2.tmpl
@@ -14,7 +14,7 @@
 				<div class="flex-item-main">
 					<div class="flex-item-title">{{.Application.Name}}</div>
 					<div class="flex-item-body">
-						<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}}</i>
+						<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}}</i>
 					</div>
 				</div>
 				<div class="flex-item-trailing">
@@ -29,7 +29,7 @@
 
 	<div class="ui g-modal-confirm delete modal" id="revoke-gitea-oauth2-grant">
 		<div class="header">
-			{{svg "octicon-shield" 16 "gt-mr-2"}}
+			{{svg "octicon-shield" 16 "tw-mr-1"}}
 			{{ctx.Locale.Tr "settings.revoke_oauth2_grant"}}
 		</div>
 		<div class="content">
diff --git a/templates/user/settings/keys.tmpl b/templates/user/settings/keys.tmpl
index 93037e7e28..e0f5e426ae 100644
--- a/templates/user/settings/keys.tmpl
+++ b/templates/user/settings/keys.tmpl
@@ -1,7 +1,11 @@
 {{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings sshkeys")}}
 	<div class="user-setting-content">
-		{{template "user/settings/keys_ssh" .}}
+		{{if not ($.UserDisabledFeatures.Contains "manage_ssh_keys")}}
+			{{template "user/settings/keys_ssh" .}}
+		{{end}}
 		{{template "user/settings/keys_principal" .}}
+		{{if not ($.UserDisabledFeatures.Contains "manage_gpg_keys")}}
 		{{template "user/settings/keys_gpg" .}}
+		{{end}}
 	</div>
 {{template "user/settings/layout_footer" .}}
diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl
index 481d7482b4..d86d838f18 100644
--- a/templates/user/settings/keys_gpg.tmpl
+++ b/templates/user/settings/keys_gpg.tmpl
@@ -5,12 +5,12 @@
 	</div>
 </h4>
 <div class="ui attached segment">
-	<div class="{{if not .HasGPGError}}gt-hidden{{end}} gt-mb-4" id="add-gpg-key-panel">
+	<div class="{{if not .HasGPGError}}tw-hidden{{end}} tw-mb-4" id="add-gpg-key-panel">
 		<form class="ui form{{if .HasGPGError}} error{{end}}" action="{{.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<input type="hidden" name="title" value="none">
 			<div class="field {{if .Err_Content}}error{{end}}">
-				<label for="content">{{ctx.Locale.Tr "settings.key_content"}}</label>
+				<label for="gpg-key-content">{{ctx.Locale.Tr "settings.key_content"}}</label>
 				<textarea id="gpg-key-content" name="content" placeholder="{{ctx.Locale.Tr "settings.key_content_gpg_placeholder"}}" required>{{.content}}</textarea>
 			</div>
 			{{if .Err_Signature}}
@@ -22,11 +22,11 @@
 					<input readonly="" value="{{.TokenToSign}}">
 					<div class="help">
 						<p>{{ctx.Locale.Tr "settings.gpg_token_help"}}</p>
-						<p><code>{{ctx.Locale.Tr "settings.gpg_token_code" .TokenToSign .PaddedKeyID}}</code></p>
+						<p><code>{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` .TokenToSign .PaddedKeyID}}</code></p>
 					</div>
 				</div>
 				<div class="field">
-					<label for="signature">{{ctx.Locale.Tr "settings.gpg_token_signature"}}</label>
+					<label for="gpg-key-signature">{{ctx.Locale.Tr "settings.gpg_token_signature"}}</label>
 					<textarea id="gpg-key-signature" name="signature" placeholder="{{ctx.Locale.Tr "settings.key_signature_gpg_placeholder"}}" required>{{.signature}}</textarea>
 				</div>
 			{{end}}
@@ -43,7 +43,7 @@
 		<div class="flex-item">
 			<p>
 				{{ctx.Locale.Tr "settings.gpg_desc"}}<br>
-				{{ctx.Locale.Tr "settings.gpg_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification#gpg-commit-signature-verification" | Str2html}}
+				{{ctx.Locale.Tr "settings.gpg_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification#gpg-commit-signature-verification"}}
 			</p>
 		</div>
 		{{range .GPGKeys}}
@@ -63,9 +63,9 @@
 						<b>{{ctx.Locale.Tr "settings.subkeys"}}:</b> {{range .SubsKey}} {{.PaddedKeyID}} {{end}}
 					</div>
 					<div class="flex-item-body">
-						<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .AddedUnix) | Safe}}</i>
+						<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .AddedUnix)}}</i>
 						-
-						<i>{{if not .ExpiredUnix.IsZero}}{{ctx.Locale.Tr "settings.valid_until_date" (DateTime "short" .ExpiredUnix) | Safe}}{{else}}{{ctx.Locale.Tr "settings.valid_forever"}}{{end}}</i>
+						<i>{{if not .ExpiredUnix.IsZero}}{{ctx.Locale.Tr "settings.valid_until_date" (DateTime "short" .ExpiredUnix)}}{{else}}{{ctx.Locale.Tr "settings.valid_forever"}}{{end}}</i>
 					</div>
 				</div>
 				<div class="flex-item-trailing">
@@ -73,7 +73,7 @@
 						{{ctx.Locale.Tr "settings.delete_key"}}
 					</button>
 					{{if and (not .Verified) (ne $.VerifyingID .KeyID)}}
-						<a class="ui primary tiny button" href="{{$.Link}}?verify_gpg={{.KeyID}}">{{ctx.Locale.Tr "settings.gpg_key_verify"}}</a>
+						<a class="ui primary tiny button" href="?verify_gpg={{.KeyID}}">{{ctx.Locale.Tr "settings.gpg_key_verify"}}</a>
 					{{end}}
 				</div>
 			</div>
@@ -90,7 +90,7 @@
 							<input readonly="" value="{{$.TokenToSign}}">
 							<div class="help">
 								<p>{{ctx.Locale.Tr "settings.gpg_token_help"}}</p>
-								<p><code>{{ctx.Locale.Tr "settings.gpg_token_code" $.TokenToSign .PaddedKeyID}}</code></p>
+								<p><code>{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` $.TokenToSign .PaddedKeyID}}</code></p>
 							</div>
 							<br>
 						</div>
diff --git a/templates/user/settings/keys_principal.tmpl b/templates/user/settings/keys_principal.tmpl
index 513afc2b61..37d8fb0e95 100644
--- a/templates/user/settings/keys_principal.tmpl
+++ b/templates/user/settings/keys_principal.tmpl
@@ -22,7 +22,7 @@
 					<div class="flex-item-main">
 						<div class="flex-item-title">{{.Name}}</div>
 						<div class="flex-item-body">
-							<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} —  {{svg "octicon-info" 16}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
+							<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} —  {{svg "octicon-info" 16}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
 						</div>
 					</div>
 					<div class="flex-item-trailing">
@@ -36,7 +36,7 @@
 	</div>
 	<br>
 
-	<div {{if not .HasPrincipalError}}class="gt-hidden"{{end}} id="add-ssh-principal-panel">
+	<div {{if not .HasPrincipalError}}class="tw-hidden"{{end}} id="add-ssh-principal-panel">
 		<h4 class="ui top attached header">
 			{{ctx.Locale.Tr "settings.add_new_principal"}}
 		</h4>
@@ -44,7 +44,7 @@
 			<form class="ui form" action="{{.Link}}" method="post">
 				{{.CsrfTokenHtml}}
 				<div class="field {{if .Err_Content}}error{{end}}">
-					<label for="content">{{ctx.Locale.Tr "settings.principal_content"}}</label>
+					<label for="ssh-principal-content">{{ctx.Locale.Tr "settings.principal_content"}}</label>
 					<input id="ssh-principal-content" name="content" value="{{.content}}" autofocus required>
 				</div>
 				<input name="title" type="hidden" value="principal">
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl
index fc8b70ea28..d31cc81b66 100644
--- a/templates/user/settings/keys_ssh.tmpl
+++ b/templates/user/settings/keys_ssh.tmpl
@@ -7,15 +7,15 @@
 	</div>
 </h4>
 <div class="ui attached segment">
-	<div class="{{if not .HasSSHError}}gt-hidden{{end}} gt-mb-4" id="add-ssh-key-panel">
+	<div class="{{if not .HasSSHError}}tw-hidden{{end}} tw-mb-4" id="add-ssh-key-panel">
 		<form class="ui form" action="{{.Link}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="field {{if .Err_Title}}error{{end}}">
-				<label for="title">{{ctx.Locale.Tr "settings.key_name"}}</label>
+				<label for="ssh-key-title">{{ctx.Locale.Tr "settings.key_name"}}</label>
 				<input id="ssh-key-title" name="title" value="{{.title}}" autofocus required maxlength="50">
 			</div>
 			<div class="field {{if .Err_Content}}error{{end}}">
-				<label for="content">{{ctx.Locale.Tr "settings.key_content"}}</label>
+				<label for="ssh-key-content">{{ctx.Locale.Tr "settings.key_content"}}</label>
 				<textarea id="ssh-key-content" name="content" class="js-quick-submit" placeholder="{{ctx.Locale.Tr "settings.key_content_ssh_placeholder"}}" required>{{.content}}</textarea>
 			</div>
 			<input name="type" type="hidden" value="ssh">
@@ -31,7 +31,7 @@
 		<div class="flex-item">
 			<p>
 				{{ctx.Locale.Tr "settings.ssh_desc"}}<br>
-				{{ctx.Locale.Tr "settings.ssh_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/connecting-to-github-with-ssh" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/troubleshooting-ssh" | Str2html}}
+				{{ctx.Locale.Tr "settings.ssh_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/connecting-to-github-with-ssh" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/troubleshooting-ssh"}}
 			</p>
 		</div>
 		{{if .DisableSSH}}
@@ -53,7 +53,7 @@
 								{{.Fingerprint}}
 						</div>
 						<div class="flex-item-body">
-								<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} —	{{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
+								<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} —	{{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
 						</div>
 				</div>
 				<div class="flex-item-trailing">
@@ -61,7 +61,7 @@
 						{{ctx.Locale.Tr "settings.delete_key"}}
 					</button>
 					{{if and (not .Verified) (ne $.VerifyingFingerprint .Fingerprint)}}
-						<a class="ui primary tiny button" href="{{$.Link}}?verify_ssh={{.Fingerprint}}">{{ctx.Locale.Tr "settings.ssh_key_verify"}}</a>
+						<a class="ui primary tiny button" href="?verify_ssh={{.Fingerprint}}">{{ctx.Locale.Tr "settings.ssh_key_verify"}}</a>
 					{{end}}
 				</div>
 			</div>
diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl
index a690d00352..c360944814 100644
--- a/templates/user/settings/navbar.tmpl
+++ b/templates/user/settings/navbar.tmpl
@@ -13,6 +13,9 @@
 		<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security">
 			{{ctx.Locale.Tr "settings.security"}}
 		</a>
+		<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
+			{{ctx.Locale.Tr "user.block.list"}}
+		</a>
 		<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/applications">
 			{{ctx.Locale.Tr "settings.applications"}}
 		</a>
diff --git a/templates/user/settings/organization.tmpl b/templates/user/settings/organization.tmpl
index 8079521984..16c27b52cd 100644
--- a/templates/user/settings/organization.tmpl
+++ b/templates/user/settings/organization.tmpl
@@ -47,7 +47,7 @@
 		{{ctx.Locale.Tr "org.members.leave"}}
 	</div>
 	<div class="content">
-		<p>{{ctx.Locale.Tr "org.members.leave.detail" `<span class="dataOrganizationName"></span>` | Safe}}</p>
+		<p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p>
 	</div>
 	{{template "base/modal_actions_confirm" .}}
 </div>
diff --git a/templates/user/settings/packages.tmpl b/templates/user/settings/packages.tmpl
index 1de20fe729..80853eab14 100644
--- a/templates/user/settings/packages.tmpl
+++ b/templates/user/settings/packages.tmpl
@@ -16,7 +16,7 @@
 					<button class="ui primary button">{{ctx.Locale.Tr "packages.owner.settings.chef.keypair"}}</button>
 				</form>
 				<div class="field">
-					<label>{{ctx.Locale.Tr "packages.registry.documentation" "Chef" "https://docs.gitea.com/usage/packages/chef/" | Safe}}</label>
+					<label>{{ctx.Locale.Tr "packages.registry.documentation" "Chef" "https://docs.gitea.com/usage/packages/chef/"}}</label>
 				</div>
 			</div>
 		</div>
diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index 1f32aed0e8..aaaf8f30db 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -9,8 +9,8 @@
 				{{.CsrfTokenHtml}}
 				<div class="required field {{if .Err_Name}}error{{end}}">
 					<label for="username">{{ctx.Locale.Tr "username"}}
-						<span class="text red gt-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span>
-						<span class="text red gt-hidden" id="name-change-redirect-prompt"> {{ctx.Locale.Tr "settings.change_username_redirect_prompt"}}</span>
+						<span class="text red tw-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span>
+						<span class="text red tw-hidden" id="name-change-redirect-prompt"> {{ctx.Locale.Tr "settings.change_username_redirect_prompt"}}</span>
 					</label>
 					<input id="username" name="name" value="{{.SignedUser.Name}}" data-name="{{.SignedUser.Name}}" autofocus required {{if or (not .SignedUser.IsLocal) .IsReverseProxy}}disabled{{end}} maxlength="40">
 					{{if or (not .SignedUser.IsLocal) .IsReverseProxy}}
@@ -22,8 +22,8 @@
 					<input id="full_name" name="full_name" value="{{.SignedUser.FullName}}" maxlength="100">
 				</div>
 				<div class="field {{if .Err_Email}}error{{end}}">
-					<label for="email">{{ctx.Locale.Tr "email"}}</label>
-					<p>{{.SignedUser.Email}}</p>
+					<label>{{ctx.Locale.Tr "email"}}</label>
+					<p id="signed-user-email">{{.SignedUser.Email}}</p>
 				</div>
 				<div class="field {{if .Err_Description}}error{{end}}">
 					<label for="description">{{ctx.Locale.Tr "user.user_bio"}}</label>
@@ -42,11 +42,11 @@
 				<!-- private block -->
 
 				<div class="field" id="privacy-user-settings">
-					<label for="security-private"><strong>{{ctx.Locale.Tr "settings.privacy"}}</strong></label>
+					<label><strong>{{ctx.Locale.Tr "settings.privacy"}}</strong></label>
 				</div>
 
 				<div class="inline field {{if .Err_Visibility}}error{{end}}">
-					<span class="inline required field"><label for="visibility">{{ctx.Locale.Tr "settings.visibility"}}</label></span>
+					<span class="inline required field"><label>{{ctx.Locale.Tr "settings.visibility"}}</label></span>
 					<div class="ui selection type dropdown">
 						{{if .SignedUser.Visibility.IsPublic}}<input type="hidden" id="visibility" name="visibility" value="0">{{end}}
 						{{if .SignedUser.Visibility.IsLimited}}<input type="hidden" id="visibility" name="visibility" value="1">{{end}}
@@ -106,7 +106,7 @@
 						<label>{{ctx.Locale.Tr "settings.lookup_avatar_by_mail"}}</label>
 					</div>
 				</div>
-				<div class="field gt-pl-4 {{if .Err_Gravatar}}error{{end}}">
+				<div class="field tw-pl-4 {{if .Err_Gravatar}}error{{end}}">
 					<label for="gravatar">Avatar {{ctx.Locale.Tr "email"}}</label>
 					<input id="gravatar" name="gravatar" value="{{.SignedUser.AvatarEmail}}">
 				</div>
@@ -119,9 +119,9 @@
 					</div>
 				</div>
 
-				<div class="inline field gt-pl-4">
-					<label for="avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
-					<input name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
+				<div class="inline field tw-pl-4">
+					<label for="new-avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
+					<input id="new-avatar" name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
 				</div>
 
 				<div class="field">
diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl
index 5aabec547a..26b9dfeed9 100644
--- a/templates/user/settings/repos.tmpl
+++ b/templates/user/settings/repos.tmpl
@@ -6,10 +6,10 @@
 		<div class="ui attached segment">
 			{{if or .allowAdopt .allowDelete}}
 				{{if .Dirs}}
-					<div class="ui middle aligned divided list">
+					<div class="ui list">
 						{{range $dirI, $dir := .Dirs}}
 							{{$repo := index $.ReposMap $dir}}
-							<div class="item {{if not $repo}}gt-py-2{{end}}">{{/* if not repo, then there are "adapt" buttons, so the padding shouldn't be that default large*/}}
+							<div class="item {{if not $repo}}tw-py-1{{end}}">{{/* if not repo, then there are "adapt" buttons, so the padding shouldn't be that default large*/}}
 								<div class="content">
 									{{if $repo}}
 										{{if $repo.IsPrivate}}
@@ -30,11 +30,11 @@
 											<span><a href="{{$repo.BaseRepo.Link}}">{{$repo.BaseRepo.OwnerName}}/{{$repo.BaseRepo.Name}}</a></span>
 										{{end}}
 									{{else}}
-										<span class="icon gt-dib gt-pt-3">{{svg "octicon-file-directory-fill"}}</span>
-										<span class="name gt-dib gt-pt-3">{{$.ContextUser.Name}}/{{$dir}}</span>
-										<div class="gt-float-right">
+										<span class="icon tw-inline-block tw-pt-2">{{svg "octicon-file-directory-fill"}}</span>
+										<span class="name tw-inline-block tw-pt-2">{{$.ContextUser.Name}}/{{$dir}}</span>
+										<div class="tw-float-right">
 											{{if $.allowAdopt}}
-												<button class="ui button primary show-modal gt-p-3" data-modal="#adopt-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-plus"}}</span><span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</span></button>
+												<button class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-plus"}}</span><span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</span></button>
 												<div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}">
 													<div class="header">
 														<span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span>
@@ -51,7 +51,7 @@
 												</div>
 											{{end}}
 											{{if $.allowDelete}}
-												<button class="ui button red show-modal gt-p-3" data-modal="#delete-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-x"}}</span><span class="label">{{ctx.Locale.Tr "repo.delete_preexisting_label"}}</span></button>
+												<button class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-x"}}</span><span class="label">{{ctx.Locale.Tr "repo.delete_preexisting_label"}}</span></button>
 												<div class="ui g-modal-confirm modal" id="delete-unadopted-modal-{{$dirI}}">
 													<div class="header">
 														<span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span>
@@ -86,15 +86,15 @@
 							<div class="item">
 								<div class="content">
 									{{if .IsPrivate}}
-										{{svg "octicon-lock" 16 "gt-mr-2 iconFloat text gold"}}
+										{{svg "octicon-lock" 16 "tw-mr-1 iconFloat text gold"}}
 									{{else if .IsFork}}
-										{{svg "octicon-repo-forked" 16 "gt-mr-2 iconFloat"}}
+										{{svg "octicon-repo-forked" 16 "tw-mr-1 iconFloat"}}
 									{{else if .IsMirror}}
-										{{svg "octicon-mirror" 16 "gt-mr-2 iconFloat"}}
+										{{svg "octicon-mirror" 16 "tw-mr-1 iconFloat"}}
 									{{else if .IsTemplate}}
-										{{svg "octicon-repo-template" 16 "gt-mr-2 iconFloat"}}
+										{{svg "octicon-repo-template" 16 "tw-mr-1 iconFloat"}}
 									{{else}}
-										{{svg "octicon-repo" 16 "gt-mr-2 iconFloat"}}
+										{{svg "octicon-repo" 16 "tw-mr-1 iconFloat"}}
 									{{end}}
 									<a class="name" href="{{.Link}}">{{.OwnerName}}/{{.Name}}</a>
 									<span>{{FileSize .Size}}</span>
diff --git a/templates/user/settings/security/openid.tmpl b/templates/user/settings/security/openid.tmpl
index 0e9b4adcbe..b0473c9df5 100644
--- a/templates/user/settings/security/openid.tmpl
+++ b/templates/user/settings/security/openid.tmpl
@@ -7,7 +7,7 @@
 			{{ctx.Locale.Tr "settings.openid_desc"}}
 		</div>
 		{{range .OpenIDs}}
-			<div class="flex-item gt-ac">
+			<div class="flex-item tw-items-center">
 				<div class="flex-item-leading">
 					{{svg "fontawesome-openid" 20}}
 				</div>
diff --git a/templates/user/settings/security/twofa.tmpl b/templates/user/settings/security/twofa.tmpl
index 2f15fe13f1..adebce4265 100644
--- a/templates/user/settings/security/twofa.tmpl
+++ b/templates/user/settings/security/twofa.tmpl
@@ -4,7 +4,7 @@
 <div class="ui attached segment">
 	<p>{{ctx.Locale.Tr "settings.twofa_desc"}}</p>
 	{{if .TOTPEnrolled}}
-	<p>{{ctx.Locale.Tr "settings.twofa_is_enrolled" | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "settings.twofa_is_enrolled"}}</p>
 	<form class="ui form" action="{{AppSubUrl}}/user/settings/security/two_factor/regenerate_scratch" method="post" enctype="multipart/form-data">
 		{{.CsrfTokenHtml}}
 		<p>{{ctx.Locale.Tr "settings.regenerate_scratch_token_desc"}}</p>
diff --git a/templates/user/settings/security/webauthn.tmpl b/templates/user/settings/security/webauthn.tmpl
index da6e5977c6..eceee191bd 100644
--- a/templates/user/settings/security/webauthn.tmpl
+++ b/templates/user/settings/security/webauthn.tmpl
@@ -1,6 +1,6 @@
 <h4 class="ui top attached header">{{ctx.Locale.Tr "settings.webauthn"}}</h4>
 <div class="ui attached segment">
-	<p>{{ctx.Locale.Tr "settings.webauthn_desc" | Str2html}}</p>
+	<p>{{ctx.Locale.Tr "settings.webauthn_desc"}}</p>
 	<p>{{ctx.Locale.Tr "settings.webauthn_key_loss_warning"}} {{ctx.Locale.Tr "settings.webauthn_alternative_tip"}}</p>
 	{{template "user/auth/webauthn_error" .}}
 	<div class="flex-list">
@@ -12,7 +12,7 @@
 				<div class="flex-item-main">
 					<div class="flex-item-title">{{.Name}}</div>
 					<div class="flex-item-body">
-						<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}}</i>
+						<i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}}</i>
 					</div>
 				</div>
 				<div class="flex-item-trailing">
diff --git a/templates/webhook/new.tmpl b/templates/webhook/new.tmpl
index a185c42b51..8ef33df25b 100644
--- a/templates/webhook/new.tmpl
+++ b/templates/webhook/new.tmpl
@@ -1,7 +1,12 @@
 <h4 class="ui top attached header">
 	{{.CustomHeaderTitle}}
-	<div class="ui right">
-		{{template "shared/webhook/icon" .ctxData}}
+	<div class="ui right type dropdown">
+		<div class="text tw-flex tw-items-center">
+			{{template "shared/webhook/icon" (dict "Size" 20 "HookType" .ctxData.HookType)}}
+			{{ctx.Locale.Tr (print "repo.settings.web_hook_name_" .ctxData.HookType)}}
+		</div>
+		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+		{{template "repo/settings/webhook/link_menu" .ctxData}}
 	</div>
 </h4>
 <div class="ui attached segment">
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
index df4fe95fdb..d15aa9a027 100644
--- a/tests/e2e/e2e_test.go
+++ b/tests/e2e/e2e_test.go
@@ -75,7 +75,7 @@ func TestMain(m *testing.M) {
 
 // TestE2e should be the only test e2e necessary. It will collect all "*.test.e2e.js" files in this directory and build a test for each.
 func TestE2e(t *testing.T) {
-	// Find the paths of all e2e test files in test test directory.
+	// Find the paths of all e2e test files in test directory.
 	searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.js")
 	paths, err := filepath.Glob(searchGlob)
 	if err != nil {
diff --git a/tests/e2e/example.test.e2e.js b/tests/e2e/example.test.e2e.js
index c741663a38..57c69a2917 100644
--- a/tests/e2e/example.test.e2e.js
+++ b/tests/e2e/example.test.e2e.js
@@ -23,7 +23,7 @@ test('Test Register Form', async ({page}, workerInfo) => {
   await page.click('form button.ui.primary.button:visible');
   // Make sure we routed to the home page. Else login failed.
   await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
-  await expect(page.locator('.dashboard-navbar span>img.ui.avatar')).toBeVisible();
+  await expect(page.locator('.secondary-nav span>img.ui.avatar')).toBeVisible();
   await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created. Welcome!');
 
   save_visual(page);
diff --git a/tests/e2e/utils_e2e.js b/tests/e2e/utils_e2e.js
index fba13ab426..d60c78b16e 100644
--- a/tests/e2e/utils_e2e.js
+++ b/tests/e2e/utils_e2e.js
@@ -52,7 +52,7 @@ export async function save_visual(page) {
       fullPage: true,
       timeout: 20000,
       mask: [
-        page.locator('.dashboard-navbar span>img.ui.avatar'),
+        page.locator('.secondary-nav span>img.ui.avatar'),
         page.locator('.ui.dropdown.jump.item span>img.ui.avatar'),
       ],
     });
diff --git a/tests/gitea-repositories-meta/org41/repo61.git/HEAD b/tests/gitea-repositories-meta/org41/repo61.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/org41/repo61.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/org41/repo61.git/config b/tests/gitea-repositories-meta/org41/repo61.git/config
new file mode 100644
index 0000000000..64280b806c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org41/repo61.git/config
@@ -0,0 +1,6 @@
+[core]
+	repositoryformatversion = 0
+	filemode = false
+	bare = true
+	symlinks = false
+	ignorecase = true
diff --git a/tests/gitea-repositories-meta/org41/repo61.git/description b/tests/gitea-repositories-meta/org41/repo61.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org41/repo61.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/org41/repo61.git/info/exclude b/tests/gitea-repositories-meta/org41/repo61.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/org41/repo61.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/81/a1c039774e337621609336c0e44ed9f92278f7 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/81/a1c039774e337621609336c0e44ed9f92278f7
new file mode 100644
index 0000000000..17a5547da8
Binary files /dev/null and b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/81/a1c039774e337621609336c0e44ed9f92278f7 differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/91/dc55f9de16a558e859123f2b99668469b1a1dc b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/91/dc55f9de16a558e859123f2b99668469b1a1dc
new file mode 100644
index 0000000000..8390a40c08
Binary files /dev/null and b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/91/dc55f9de16a558e859123f2b99668469b1a1dc differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/a5/bbc0fd39a696feabed2d4cccaf05abbcaf3b02 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/a5/bbc0fd39a696feabed2d4cccaf05abbcaf3b02
new file mode 100644
index 0000000000..94312d3db6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/a5/bbc0fd39a696feabed2d4cccaf05abbcaf3b02
@@ -0,0 +1 @@
+x��Kn� D��죱�v�EQ��~n�@3F���r�\,d��^�T��S�ϏGj|��K+D����
$2`�4��Y��Y�u{�Xho\���u4E;k-	P4�Q^H�84��lk.��i�_��|g�V�v��=����|�-U�q8�;�ZJ��,��Nn�_����0�a�3���ҿ	T�mķ�q�1m�؍b�򵵣^���z��/����_�5�zR'-'�~�tl�
\ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/cf/19952a40b92eb2f86689146a65ac2d87c0818a b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/cf/19952a40b92eb2f86689146a65ac2d87c0818a
new file mode 100644
index 0000000000..b384e5c72e
Binary files /dev/null and b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/cf/19952a40b92eb2f86689146a65ac2d87c0818a differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e1/6da91326b845f1ba86a7df0a67db352f96dcb0 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e1/6da91326b845f1ba86a7df0a67db352f96dcb0
new file mode 100644
index 0000000000..da281ff791
Binary files /dev/null and b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e1/6da91326b845f1ba86a7df0a67db352f96dcb0 differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master b/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master
index 38984b12b7..b352f15003 100644
--- a/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master
@@ -1 +1 @@
-0dca5bd9b5d7ef937710e056f575e86c0184ba85
+a5bbc0fd39a696feabed2d4cccaf05abbcaf3b02
diff --git a/tests/gitea-repositories-meta/user40/repo60.git/HEAD b/tests/gitea-repositories-meta/user40/repo60.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user40/repo60.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user40/repo60.git/config b/tests/gitea-repositories-meta/user40/repo60.git/config
new file mode 100644
index 0000000000..64280b806c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user40/repo60.git/config
@@ -0,0 +1,6 @@
+[core]
+	repositoryformatversion = 0
+	filemode = false
+	bare = true
+	symlinks = false
+	ignorecase = true
diff --git a/tests/gitea-repositories-meta/user40/repo60.git/description b/tests/gitea-repositories-meta/user40/repo60.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user40/repo60.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user40/repo60.git/info/exclude b/tests/gitea-repositories-meta/user40/repo60.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user40/repo60.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go
index 7744f33e57..2a2fdceb61 100644
--- a/tests/integration/actions_trigger_test.go
+++ b/tests/integration/actions_trigger_test.go
@@ -12,6 +12,7 @@ import (
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
@@ -19,8 +20,11 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	actions_module "code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
 	pull_service "code.gitea.io/gitea/services/pull"
+	release_service "code.gitea.io/gitea/services/release"
 	repo_service "code.gitea.io/gitea/services/repository"
 	files_service "code.gitea.io/gitea/services/repository/files"
 
@@ -199,6 +203,7 @@ func TestPullRequestTargetEvent(t *testing.T) {
 
 func TestSkipCI(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		session := loginUser(t, "user2")
 		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 
 		// create the repo
@@ -209,7 +214,7 @@ func TestSkipCI(t *testing.T) {
 			Gitignores:    "Go",
 			License:       "MIT",
 			Readme:        "Default",
-			DefaultBranch: "main",
+			DefaultBranch: "master",
 			IsPrivate:     false,
 		})
 		assert.NoError(t, err)
@@ -228,12 +233,12 @@ func TestSkipCI(t *testing.T) {
 				{
 					Operation:     "create",
 					TreePath:      ".gitea/workflows/pr.yml",
-					ContentReader: strings.NewReader("name: test\non:\n  push:\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo helloworld\n"),
+					ContentReader: strings.NewReader("name: test\non:\n  push:\n    branches: [master]\n  pull_request:\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo helloworld\n"),
 				},
 			},
 			Message:   "add workflow",
-			OldBranch: "main",
-			NewBranch: "main",
+			OldBranch: "master",
+			NewBranch: "master",
 			Author: &files_service.IdentityOptions{
 				Name:  user2.Name,
 				Email: user2.Email,
@@ -263,8 +268,8 @@ func TestSkipCI(t *testing.T) {
 				},
 			},
 			Message:   fmt.Sprintf("%s add bar", setting.Actions.SkipWorkflowStrings[0]),
-			OldBranch: "main",
-			NewBranch: "main",
+			OldBranch: "master",
+			NewBranch: "master",
 			Author: &files_service.IdentityOptions{
 				Name:  user2.Name,
 				Email: user2.Email,
@@ -283,5 +288,158 @@ func TestSkipCI(t *testing.T) {
 
 		// the commit message contains a configured skip-ci string, so there is still only 1 record
 		assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
+
+		// add file to new branch
+		addFileToBranchResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+			Files: []*files_service.ChangeRepoFile{
+				{
+					Operation:     "create",
+					TreePath:      "test-skip-ci",
+					ContentReader: strings.NewReader("test-skip-ci"),
+				},
+			},
+			Message:   "add test file",
+			OldBranch: "master",
+			NewBranch: "test-skip-ci",
+			Author: &files_service.IdentityOptions{
+				Name:  user2.Name,
+				Email: user2.Email,
+			},
+			Committer: &files_service.IdentityOptions{
+				Name:  user2.Name,
+				Email: user2.Email,
+			},
+			Dates: &files_service.CommitDateOptions{
+				Author:    time.Now(),
+				Committer: time.Now(),
+			},
+		})
+		assert.NoError(t, err)
+		assert.NotEmpty(t, addFileToBranchResp)
+
+		resp := testPullCreate(t, session, "user2", "skip-ci", true, "master", "test-skip-ci", "[skip ci] test-skip-ci")
+
+		// check the redirected URL
+		url := test.RedirectURL(resp)
+		assert.Regexp(t, "^/user2/skip-ci/pulls/[0-9]*$", url)
+
+		// the pr title contains a configured skip-ci string, so there is still only 1 record
+		assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
+	})
+}
+
+func TestCreateDeleteRefEvent(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+		// create the repo
+		repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
+			Name:          "create-delete-ref-event",
+			Description:   "test create delete ref ci event",
+			AutoInit:      true,
+			Gitignores:    "Go",
+			License:       "MIT",
+			Readme:        "Default",
+			DefaultBranch: "main",
+			IsPrivate:     false,
+		})
+		assert.NoError(t, err)
+		assert.NotEmpty(t, repo)
+
+		// enable actions
+		err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{
+			RepoID: repo.ID,
+			Type:   unit_model.TypeActions,
+		}}, nil)
+		assert.NoError(t, err)
+
+		// add workflow file to the repo
+		addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+			Files: []*files_service.ChangeRepoFile{
+				{
+					Operation:     "create",
+					TreePath:      ".gitea/workflows/createdelete.yml",
+					ContentReader: strings.NewReader("name: test\non:\n  [create,delete]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo helloworld\n"),
+				},
+			},
+			Message:   "add workflow",
+			OldBranch: "main",
+			NewBranch: "main",
+			Author: &files_service.IdentityOptions{
+				Name:  user2.Name,
+				Email: user2.Email,
+			},
+			Committer: &files_service.IdentityOptions{
+				Name:  user2.Name,
+				Email: user2.Email,
+			},
+			Dates: &files_service.CommitDateOptions{
+				Author:    time.Now(),
+				Committer: time.Now(),
+			},
+		})
+		assert.NoError(t, err)
+		assert.NotEmpty(t, addWorkflowToBaseResp)
+
+		// Get the commit ID of the default branch
+		gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
+		assert.NoError(t, err)
+		defer gitRepo.Close()
+		branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch)
+		assert.NoError(t, err)
+
+		// create a branch
+		err = repo_service.CreateNewBranchFromCommit(db.DefaultContext, user2, repo, gitRepo, branch.CommitID, "test-create-branch")
+		assert.NoError(t, err)
+		run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+			Title:      "add workflow",
+			RepoID:     repo.ID,
+			Event:      "create",
+			Ref:        "refs/heads/test-create-branch",
+			WorkflowID: "createdelete.yml",
+			CommitSHA:  branch.CommitID,
+		})
+		assert.NotNil(t, run)
+
+		// create a tag
+		err = release_service.CreateNewTag(db.DefaultContext, user2, repo, branch.CommitID, "test-create-tag", "test create tag event")
+		assert.NoError(t, err)
+		run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+			Title:      "add workflow",
+			RepoID:     repo.ID,
+			Event:      "create",
+			Ref:        "refs/tags/test-create-tag",
+			WorkflowID: "createdelete.yml",
+			CommitSHA:  branch.CommitID,
+		})
+		assert.NotNil(t, run)
+
+		// delete the branch
+		err = repo_service.DeleteBranch(db.DefaultContext, user2, repo, gitRepo, "test-create-branch")
+		assert.NoError(t, err)
+		run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+			Title:      "add workflow",
+			RepoID:     repo.ID,
+			Event:      "delete",
+			Ref:        "main",
+			WorkflowID: "createdelete.yml",
+			CommitSHA:  branch.CommitID,
+		})
+		assert.NotNil(t, run)
+
+		// delete the tag
+		tag, err := repo_model.GetRelease(db.DefaultContext, repo.ID, "test-create-tag")
+		assert.NoError(t, err)
+		err = release_service.DeleteReleaseByID(db.DefaultContext, repo, tag, user2, true)
+		assert.NoError(t, err)
+		run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+			Title:      "add workflow",
+			RepoID:     repo.ID,
+			Event:      "delete",
+			Ref:        "main",
+			WorkflowID: "createdelete.yml",
+			CommitSHA:  branch.CommitID,
+		})
+		assert.NotNil(t, run)
 	})
 }
diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go
new file mode 100644
index 0000000000..f58f876849
--- /dev/null
+++ b/tests/integration/api_actions_artifact_v4_test.go
@@ -0,0 +1,224 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"encoding/hex"
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"code.gitea.io/gitea/routers/api/actions"
+	actions_service "code.gitea.io/gitea/services/actions"
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+	"google.golang.org/protobuf/encoding/protojson"
+	"google.golang.org/protobuf/reflect/protoreflect"
+	"google.golang.org/protobuf/types/known/timestamppb"
+	"google.golang.org/protobuf/types/known/wrapperspb"
+)
+
+func toProtoJSON(m protoreflect.ProtoMessage) io.Reader {
+	resp, _ := protojson.Marshal(m)
+	buf := bytes.Buffer{}
+	buf.Write(resp)
+	return &buf
+}
+
+func TestActionsArtifactV4UploadSingleFile(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
+	assert.NoError(t, err)
+
+	// acquire artifact upload url
+	req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
+		Version:                 4,
+		Name:                    "artifact",
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+	var uploadResp actions.CreateArtifactResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
+	assert.True(t, uploadResp.Ok)
+	assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
+
+	// get upload url
+	idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
+	url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
+
+	// upload artifact chunk
+	body := strings.Repeat("A", 1024)
+	req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
+	MakeRequest(t, req, http.StatusCreated)
+
+	t.Logf("Create artifact confirm")
+
+	sha := sha256.Sum256([]byte(body))
+
+	// confirm artifact upload
+	req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
+		Name:                    "artifact",
+		Size:                    1024,
+		Hash:                    wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).
+		AddTokenAuth(token)
+	resp = MakeRequest(t, req, http.StatusOK)
+	var finalizeResp actions.FinalizeArtifactResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
+	assert.True(t, finalizeResp.Ok)
+}
+
+func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
+	assert.NoError(t, err)
+
+	// acquire artifact upload url
+	req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
+		Version:                 4,
+		Name:                    "artifact-invalid-checksum",
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+	var uploadResp actions.CreateArtifactResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
+	assert.True(t, uploadResp.Ok)
+	assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
+
+	// get upload url
+	idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
+	url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
+
+	// upload artifact chunk
+	body := strings.Repeat("B", 1024)
+	req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
+	MakeRequest(t, req, http.StatusCreated)
+
+	t.Logf("Create artifact confirm")
+
+	sha := sha256.Sum256([]byte(strings.Repeat("A", 1024)))
+
+	// confirm artifact upload
+	req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
+		Name:                    "artifact-invalid-checksum",
+		Size:                    1024,
+		Hash:                    wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).
+		AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusInternalServerError)
+}
+
+func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
+	assert.NoError(t, err)
+
+	// acquire artifact upload url
+	req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
+		Version:                 4,
+		ExpiresAt:               timestamppb.New(time.Now().Add(5 * 24 * time.Hour)),
+		Name:                    "artifactWithRetentionDays",
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+	var uploadResp actions.CreateArtifactResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
+	assert.True(t, uploadResp.Ok)
+	assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
+
+	// get upload url
+	idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
+	url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
+
+	// upload artifact chunk
+	body := strings.Repeat("A", 1024)
+	req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
+	MakeRequest(t, req, http.StatusCreated)
+
+	t.Logf("Create artifact confirm")
+
+	sha := sha256.Sum256([]byte(body))
+
+	// confirm artifact upload
+	req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
+		Name:                    "artifactWithRetentionDays",
+		Size:                    1024,
+		Hash:                    wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).
+		AddTokenAuth(token)
+	resp = MakeRequest(t, req, http.StatusOK)
+	var finalizeResp actions.FinalizeArtifactResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
+	assert.True(t, finalizeResp.Ok)
+}
+
+func TestActionsArtifactV4DownloadSingle(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
+	assert.NoError(t, err)
+
+	// acquire artifact upload url
+	req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
+		NameFilter:              wrapperspb.String("artifact"),
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+	var listResp actions.ListArtifactsResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &listResp)
+	assert.Len(t, listResp.Artifacts, 1)
+
+	// confirm artifact upload
+	req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{
+		Name:                    "artifact",
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).
+		AddTokenAuth(token)
+	resp = MakeRequest(t, req, http.StatusOK)
+	var finalizeResp actions.GetSignedArtifactURLResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
+	assert.NotEmpty(t, finalizeResp.SignedUrl)
+
+	req = NewRequest(t, "GET", finalizeResp.SignedUrl)
+	resp = MakeRequest(t, req, http.StatusOK)
+	body := strings.Repeat("A", 1024)
+	assert.Equal(t, resp.Body.String(), body)
+}
+
+func TestActionsArtifactV4Delete(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
+	assert.NoError(t, err)
+
+	// delete artifact by name
+	req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/DeleteArtifact", toProtoJSON(&actions.DeleteArtifactRequest{
+		Name:                    "artifact",
+		WorkflowRunBackendId:    "792",
+		WorkflowJobRunBackendId: "193",
+	})).AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+	var deleteResp actions.DeleteArtifactResponse
+	protojson.Unmarshal(resp.Body.Bytes(), &deleteResp)
+	assert.True(t, deleteResp.Ok)
+}
diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go
index 0748a75ba4..e8954f5b20 100644
--- a/tests/integration/api_admin_test.go
+++ b/tests/integration/api_admin_test.go
@@ -14,9 +14,11 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/tests"
 
+	"github.com/gobwas/glob"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -333,3 +335,58 @@ func TestAPICron(t *testing.T) {
 		}
 	})
 }
+
+func TestAPICreateUser_NotAllowedEmailDomain(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("example.org")}
+	defer func() {
+		setting.Service.EmailDomainAllowList = []glob.Glob{}
+	}()
+
+	adminUsername := "user1"
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
+
+	req := NewRequestWithValues(t, "POST", "/api/v1/admin/users", map[string]string{
+		"email":                "allowedUser1@example1.org",
+		"login_name":           "allowedUser1",
+		"username":             "allowedUser1",
+		"password":             "allowedUser1_pass",
+		"must_change_password": "true",
+	}).AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusCreated)
+	assert.Equal(t, "the domain of user email allowedUser1@example1.org conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", resp.Header().Get("X-Gitea-Warning"))
+
+	req = NewRequest(t, "DELETE", "/api/v1/admin/users/allowedUser1").AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusNoContent)
+}
+
+func TestAPIEditUser_NotAllowedEmailDomain(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("example.org")}
+	defer func() {
+		setting.Service.EmailDomainAllowList = []glob.Glob{}
+	}()
+
+	adminUsername := "user1"
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
+	urlStr := fmt.Sprintf("/api/v1/admin/users/%s", "user2")
+
+	newEmail := "user2@example1.com"
+	req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
+		LoginName: "user2",
+		SourceID:  0,
+		Email:     &newEmail,
+	}).AddTokenAuth(token)
+	resp := MakeRequest(t, req, http.StatusOK)
+	assert.Equal(t, "the domain of user email user2@example1.com conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", resp.Header().Get("X-Gitea-Warning"))
+
+	originalEmail := "user2@example.com"
+	req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
+		LoginName: "user2",
+		SourceID:  0,
+		Email:     &originalEmail,
+	}).AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusOK)
+}
diff --git a/tests/integration/api_comment_test.go b/tests/integration/api_comment_test.go
index a9c5228a16..255b8332b2 100644
--- a/tests/integration/api_comment_test.go
+++ b/tests/integration/api_comment_test.go
@@ -108,6 +108,32 @@ func TestAPICreateComment(t *testing.T) {
 	DecodeJSON(t, resp, &updatedComment)
 	assert.EqualValues(t, commentBody, updatedComment.Body)
 	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
+
+	t.Run("BlockedByRepoOwner", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+
+		req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", repo.OwnerName, repo.Name, issue.Index), map[string]string{
+			"body": commentBody,
+		}).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+		MakeRequest(t, req, http.StatusForbidden)
+	})
+
+	t.Run("BlockedByIssuePoster", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 13})
+		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+
+		req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", repo.OwnerName, repo.Name, issue.Index), map[string]string{
+			"body": commentBody,
+		}).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+		MakeRequest(t, req, http.StatusForbidden)
+	})
 }
 
 func TestAPIGetComment(t *testing.T) {
diff --git a/tests/integration/api_issue_reaction_test.go b/tests/integration/api_issue_reaction_test.go
index 4ca909f281..17e9f7aed5 100644
--- a/tests/integration/api_issue_reaction_test.go
+++ b/tests/integration/api_issue_reaction_test.go
@@ -58,6 +58,13 @@ func TestAPIIssuesReactions(t *testing.T) {
 	// Add existing reaction
 	MakeRequest(t, req, http.StatusForbidden)
 
+	// Blocked user can't react to comment
+	user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+	req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
+		Reaction: "rocket",
+	}).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteIssue))
+	MakeRequest(t, req, http.StatusForbidden)
+
 	// Get end result of reaction list of issue #1
 	req = NewRequest(t, "GET", urlStr).
 		AddTokenAuth(token)
diff --git a/tests/integration/api_issue_templates_test.go b/tests/integration/api_issue_templates_test.go
new file mode 100644
index 0000000000..6b65e6e086
--- /dev/null
+++ b/tests/integration/api_issue_templates_test.go
@@ -0,0 +1,55 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"net/http"
+	"net/url"
+	"testing"
+
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	api "code.gitea.io/gitea/modules/structs"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAPIIssueTemplateList(t *testing.T) {
+	onGiteaRun(t, func(*testing.T, *url.URL) {
+		var issueTemplates []*api.IssueTemplate
+
+		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
+		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
+
+		// no issue template
+		req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/issue_templates")
+		resp := MakeRequest(t, req, http.StatusOK)
+		issueTemplates = nil
+		DecodeJSON(t, resp, &issueTemplates)
+		assert.Empty(t, issueTemplates)
+
+		// one correct issue template and some incorrect issue templates
+		err := createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/tmpl-ok.md", repo.DefaultBranch, `----
+name: foo
+about: bar
+----
+`)
+		assert.NoError(t, err)
+
+		err = createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/tmpl-err1.yml", repo.DefaultBranch, `name: '`)
+		assert.NoError(t, err)
+
+		err = createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/tmpl-err2.yml", repo.DefaultBranch, `other: `)
+		assert.NoError(t, err)
+
+		req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/issue_templates")
+		resp = MakeRequest(t, req, http.StatusOK)
+		issueTemplates = nil
+		DecodeJSON(t, resp, &issueTemplates)
+		assert.Len(t, issueTemplates, 1)
+		assert.Equal(t, "foo", issueTemplates[0].Name)
+		assert.Equal(t, "error occurs when parsing issue template: count=2", resp.Header().Get("X-Gitea-Warning"))
+	})
+}
diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go
index f025806868..17b4e5bd71 100644
--- a/tests/integration/api_issue_test.go
+++ b/tests/integration/api_issue_test.go
@@ -84,7 +84,7 @@ func TestAPICreateIssue(t *testing.T) {
 
 	session := loginUser(t, owner.Name)
 	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
-	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all", owner.Name, repoBefore.Name)
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name)
 	req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
 		Body:     body,
 		Title:    title,
@@ -106,6 +106,12 @@ func TestAPICreateIssue(t *testing.T) {
 	repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	assert.Equal(t, repoBefore.NumIssues+1, repoAfter.NumIssues)
 	assert.Equal(t, repoBefore.NumClosedIssues, repoAfter.NumClosedIssues)
+
+	user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+	req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
+		Title: title,
+	}).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteIssue))
+	MakeRequest(t, req, http.StatusForbidden)
 }
 
 func TestAPICreateIssueParallel(t *testing.T) {
@@ -117,7 +123,7 @@ func TestAPICreateIssueParallel(t *testing.T) {
 
 	session := loginUser(t, owner.Name)
 	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
-	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all", owner.Name, repoBefore.Name)
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name)
 
 	var wg sync.WaitGroup
 	for i := 0; i < 10; i++ {
@@ -217,7 +223,7 @@ func TestAPISearchIssues(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	// as this API was used in the frontend, it uses UI page size
-	expectedIssueCount := 18 // from the fixtures
+	expectedIssueCount := 20 // from the fixtures
 	if expectedIssueCount > setting.UI.IssuePagingNum {
 		expectedIssueCount = setting.UI.IssuePagingNum
 	}
@@ -257,7 +263,7 @@ func TestAPISearchIssues(t *testing.T) {
 	req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
 	resp = MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &apiIssues)
-	assert.EqualValues(t, "20", resp.Header().Get("X-Total-Count"))
+	assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count"))
 	assert.Len(t, apiIssues, 20)
 
 	query.Add("limit", "10")
@@ -265,7 +271,7 @@ func TestAPISearchIssues(t *testing.T) {
 	req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
 	resp = MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &apiIssues)
-	assert.EqualValues(t, "20", resp.Header().Get("X-Total-Count"))
+	assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count"))
 	assert.Len(t, apiIssues, 10)
 
 	query = url.Values{"assigned": {"true"}, "state": {"all"}}
@@ -315,7 +321,7 @@ func TestAPISearchIssuesWithLabels(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	// as this API was used in the frontend, it uses UI page size
-	expectedIssueCount := 18 // from the fixtures
+	expectedIssueCount := 20 // from the fixtures
 	if expectedIssueCount > setting.UI.IssuePagingNum {
 		expectedIssueCount = setting.UI.IssuePagingNum
 	}
diff --git a/tests/integration/api_nodeinfo_test.go b/tests/integration/api_nodeinfo_test.go
index 876fb5ac13..75f8dbb4ba 100644
--- a/tests/integration/api_nodeinfo_test.go
+++ b/tests/integration/api_nodeinfo_test.go
@@ -32,8 +32,8 @@ func TestNodeinfo(t *testing.T) {
 		DecodeJSON(t, resp, &nodeinfo)
 		assert.True(t, nodeinfo.OpenRegistrations)
 		assert.Equal(t, "gitea", nodeinfo.Software.Name)
-		assert.Equal(t, 26, nodeinfo.Usage.Users.Total)
-		assert.Equal(t, 20, nodeinfo.Usage.LocalPosts)
+		assert.Equal(t, 29, nodeinfo.Usage.Users.Total)
+		assert.Equal(t, 22, nodeinfo.Usage.LocalPosts)
 		assert.Equal(t, 3, nodeinfo.Usage.LocalComments)
 	})
 }
diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go
index 1cd82fe4e0..70d3a446f7 100644
--- a/tests/integration/api_org_test.go
+++ b/tests/integration/api_org_test.go
@@ -177,7 +177,7 @@ func TestAPIGetAll(t *testing.T) {
 	var apiOrgList []*api.Organization
 
 	DecodeJSON(t, resp, &apiOrgList)
-	assert.Len(t, apiOrgList, 11)
+	assert.Len(t, apiOrgList, 12)
 	assert.Equal(t, "Limited Org 36", apiOrgList[1].FullName)
 	assert.Equal(t, "limited", apiOrgList[1].Visibility)
 
@@ -186,7 +186,7 @@ func TestAPIGetAll(t *testing.T) {
 	resp = MakeRequest(t, req, http.StatusOK)
 
 	DecodeJSON(t, resp, &apiOrgList)
-	assert.Len(t, apiOrgList, 7)
+	assert.Len(t, apiOrgList, 8)
 	assert.Equal(t, "org 17", apiOrgList[0].FullName)
 	assert.Equal(t, "public", apiOrgList[0].Visibility)
 }
diff --git a/tests/integration/api_packages_alpine_test.go b/tests/integration/api_packages_alpine_test.go
index 3cc7178e02..228f497127 100644
--- a/tests/integration/api_packages_alpine_test.go
+++ b/tests/integration/api_packages_alpine_test.go
@@ -19,6 +19,7 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
+	alpine_service "code.gitea.io/gitea/services/packages/alpine"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
@@ -59,7 +60,34 @@ Djfa/2q5bH4699v++uMAAAAAAAAAAAAAAAAAAAAAAHbgA/eXQh8AKAAA`
 	content, err := base64.StdEncoding.DecodeString(base64AlpinePackageContent)
 	assert.NoError(t, err)
 
-	branches := []string{"v3.16", "v3.17", "v3.18"}
+	base64AlpinePackageNoArchContent := `H4sIAAAAAAACA9ML9nT30wsKdtQrLU4t0jUzTUo1NDVP0ysqTtQrKE1ioAYwAAIzExMwDQTotCGI
+bWhiampuYmRiaGrMYGBoZGZkxKBgwEAHUFpcklikoMAwQkHLB7eoE40P9n5jvx32t7Dy9rq7x19k
+66cJPV38t/h+vWe2jdXy+/PzPT0YTF5z39i4cPFptcLa1C1lD0z/XvrNp6In/7nP4PPCF2pZu8uV
+z74QXLxpY1XWJuVFysqVf+PdizccFbD6ZL/QPGXd1Ri1fec2XBNuYfK/rFa6wF/h3dK/W12f8mxP
+04iP3aCy+vPx7h9S+5M1LLkWr5M/4ezGt3bDW/FjBp/S9hiKP72s/XrJ0vWtO0zr5wa+D/X8XluW
+d7BLP7XS3YUhd8WbPPF/NW3691ONJbXsRb69O7BIMZC96uTri+utC/fbie5J+n7zhCxD4Aep/qet
+QnlCZyN8MhNdVNlNl7965R1nExrrGvfI/YQZFx8Dg+d9122hZsYd/24WL/L69OWrDAN/y//nS7im
+XEive3v7QeTe433TPj/X71+9yHiV6+E9k++3TL8V0Xoq9panhNt23fLgau/pTOvmKx6bV/pS26+Y
+5UP4viyuklYeu4/BZl6rLINe1L/uWuUXcH5z7pa2b9+/rp/v/8dFgc1PL3bO3/iVcrI//J/LMU2X
+Nzu1IaMmWXnGp7CmyQIR39d0Nai9/+tdPbfjvmsNH88Tu7uVrvNuJE0wjxfePXGv/KHNXD+mnG0t
+yTPu+Na0b5WR9O4t0yMd9T5k6ui7hOyU/jL/4dOn6neLwhdrZIZfcl1ectnGvUTurWDo1vY5Gw9k
+PTQLVgcA61F+7gAEAAAfiwgAAAAAAAID7VVNa9wwEPXZv2Ig53hHlizbCzkVkobQJtDkB4wl2SvW
+lhdbTpP++oyXQGEPLYU2paTvIs3X05PQSNnmjp4+OrJumjfZ3c3V9efL2+T3AhlaqePIOB0Rc50I
+VRSlypUoZIJCKJQJPCVvgGWONLGU5H1CCDDRD+4CU57S6zT5j3eCP9Tyv9T/GsuT/scyLxPAt+z/
+aRzjj/J+Fv9HcQZXLriJorPQPAM1i+8tyEzkGZ5PmJ7BMvvQQUt7tx4BPPJH4ccAIpN5Jjj+hSJc
+ugZAghDbArco4eH+A+SYq/Sw7wINDi6g89HReRhpMrvVzTzsFZlaV2Hbutmw4zVhmXo2djEe5u1m
+c6zNzDikR3mW1a61JepaC0SZHsjsqTsyPoR9GL+GdPbf1iSFtU5Xyu/c4+Q7H04lMfvgI3vT3hsX
+5rX40/U9b5CWOA78Mhrq+2ewLjrDp7VNWQbtaF6ZXVWZIhdV09RWOIvU6BqNboSxLSEpkrpQq80x
+W1Nla6NavuqtrJQ0sv17D+4L2oD1lwAIAAAfiwgAAAAAAAID7dM/SgNBFAbw6cSAnYXlXsDNm50/
+u1METBeIkEBMK87uzKKEJbB/IN7CxhN4AI/gNcRD6BWciI0WSiBGxO/XvA9mile8L+5P7WrkrfN1
+049dV1XXbNso0FK+zeDzJC4SxqVSqUwkV4IR51KkLFqxHeia1tZhFfY/cR4V7VXlB9QL0b5HnUXD
+6fj4bDI5ncXFpS8WTVfFs9GQD5wVxgrvlde5zMmJRKm89KVRmnhmyJYuo5RMj8Ef8EOV36j/6/yx
+/5qnxKJ1J8MZJifskD2Zu+fzxfggmT+83F4c3dw/7u1vtf/1ctl+9e+7dwAAAAAAAAAAAAAAAAAA
+AACAX/AKARNTyAAoAAA=`
+	noarchContent, err := base64.StdEncoding.DecodeString(base64AlpinePackageNoArchContent)
+	assert.NoError(t, err)
+
+	branches := []string{"v3.16", "v3.17"}
 	repositories := []string{"main", "testing"}
 
 	rootURL := fmt.Sprintf("/api/packages/%s/alpine", user.Name)
@@ -139,63 +167,71 @@ Djfa/2q5bH4699v++uMAAAAAAAAAAAAAAAAAAAAAAHbgA/eXQh8AKAAA`
 					})
 				})
 
-				t.Run("Index", func(t *testing.T) {
-					defer tests.PrintCurrentTest(t)()
+				readIndexContent := func(r io.Reader) (string, error) {
+					br := bufio.NewReader(r)
 
-					url := fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository)
+					gzr, err := gzip.NewReader(br)
+					if err != nil {
+						return "", err
+					}
 
-					req := NewRequest(t, "GET", url)
-					resp := MakeRequest(t, req, http.StatusOK)
-
-					assert.Condition(t, func() bool {
-						br := bufio.NewReader(resp.Body)
-
-						gzr, err := gzip.NewReader(br)
-						assert.NoError(t, err)
+					for {
+						gzr.Multistream(false)
 
+						tr := tar.NewReader(gzr)
 						for {
-							gzr.Multistream(false)
-
-							tr := tar.NewReader(gzr)
-							for {
-								hd, err := tr.Next()
-								if err == io.EOF {
-									break
-								}
-								assert.NoError(t, err)
-
-								if hd.Name == "APKINDEX" {
-									buf, err := io.ReadAll(tr)
-									assert.NoError(t, err)
-
-									s := string(buf)
-
-									assert.Contains(t, s, "C:Q1/se1PjO94hYXbfpNR1/61hVORIc=\n")
-									assert.Contains(t, s, "P:"+packageName+"\n")
-									assert.Contains(t, s, "V:"+packageVersion+"\n")
-									assert.Contains(t, s, "A:x86_64\n")
-									assert.Contains(t, s, "T:Gitea Test Package\n")
-									assert.Contains(t, s, "U:https://gitea.io/\n")
-									assert.Contains(t, s, "L:MIT\n")
-									assert.Contains(t, s, "S:1353\n")
-									assert.Contains(t, s, "I:4096\n")
-									assert.Contains(t, s, "o:gitea-test\n")
-									assert.Contains(t, s, "m:KN4CK3R <kn4ck3r@gitea.io>\n")
-									assert.Contains(t, s, "t:1679498030\n")
-
-									return true
-								}
-							}
-
-							err = gzr.Reset(br)
+							hd, err := tr.Next()
 							if err == io.EOF {
 								break
 							}
-							assert.NoError(t, err)
+							if err != nil {
+								return "", err
+							}
+
+							if hd.Name == alpine_service.IndexFilename {
+								buf, err := io.ReadAll(tr)
+								if err != nil {
+									return "", err
+								}
+
+								return string(buf), nil
+							}
 						}
 
-						return false
-					})
+						err = gzr.Reset(br)
+						if err == io.EOF {
+							break
+						}
+						if err != nil {
+							return "", err
+						}
+					}
+
+					return "", io.EOF
+				}
+
+				t.Run("Index", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository))
+					resp := MakeRequest(t, req, http.StatusOK)
+
+					content, err := readIndexContent(resp.Body)
+					assert.NoError(t, err)
+
+					assert.Contains(t, content, "C:Q1/se1PjO94hYXbfpNR1/61hVORIc=\n")
+					assert.Contains(t, content, "P:"+packageName+"\n")
+					assert.Contains(t, content, "V:"+packageVersion+"\n")
+					assert.Contains(t, content, "A:x86_64\n")
+					assert.NotContains(t, content, "A:noarch\n")
+					assert.Contains(t, content, "T:Gitea Test Package\n")
+					assert.Contains(t, content, "U:https://gitea.io/\n")
+					assert.Contains(t, content, "L:MIT\n")
+					assert.Contains(t, content, "S:1353\n")
+					assert.Contains(t, content, "I:4096\n")
+					assert.Contains(t, content, "o:gitea-test\n")
+					assert.Contains(t, content, "m:KN4CK3R <kn4ck3r@gitea.io>\n")
+					assert.Contains(t, content, "t:1679498030\n")
 				})
 
 				t.Run("Download", func(t *testing.T) {
@@ -204,6 +240,35 @@ Djfa/2q5bH4699v++uMAAAAAAAAAAAAAAAAAAAAAAHbgA/eXQh8AKAAA`
 					req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, packageName, packageVersion))
 					MakeRequest(t, req, http.StatusOK)
 				})
+
+				t.Run("NoArch", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s/%s", rootURL, branch, repository), bytes.NewReader(noarchContent)).
+						AddBasicAuth(user.Name)
+					MakeRequest(t, req, http.StatusCreated)
+
+					req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository))
+					resp := MakeRequest(t, req, http.StatusOK)
+
+					content, err := readIndexContent(resp.Body)
+					assert.NoError(t, err)
+
+					assert.Contains(t, content, "C:Q1/se1PjO94hYXbfpNR1/61hVORIc=\n")
+					assert.Contains(t, content, "A:x86_64\n")
+					assert.Contains(t, content, "C:Q1kbH5WoIPFccQYyATanaKXd2cJcc=\n")
+					assert.NotContains(t, content, "A:noarch\n")
+
+					// noarch package should be available with every architecture requested
+					for _, arch := range []string{alpine_module.NoArch, "x86_64", "my_arch"} {
+						req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s/gitea-noarch-1.4-r0.apk", rootURL, branch, repository, arch))
+						MakeRequest(t, req, http.StatusOK)
+					}
+
+					req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/noarch/gitea-noarch-1.4-r0.apk", rootURL, branch, repository)).
+						AddBasicAuth(user.Name)
+					MakeRequest(t, req, http.StatusNoContent)
+				})
 			})
 		}
 	}
diff --git a/tests/integration/api_packages_chef_test.go b/tests/integration/api_packages_chef_test.go
index 4123c7216c..05545f11a6 100644
--- a/tests/integration/api_packages_chef_test.go
+++ b/tests/integration/api_packages_chef_test.go
@@ -11,6 +11,7 @@ import (
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/pem"
@@ -33,7 +34,6 @@ import (
 	chef_router "code.gitea.io/gitea/routers/api/packages/chef"
 	"code.gitea.io/gitea/tests"
 
-	"github.com/minio/sha256-simd"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go
index 509ad424e6..9ac6e5256b 100644
--- a/tests/integration/api_packages_container_test.go
+++ b/tests/integration/api_packages_container_test.go
@@ -5,6 +5,7 @@ package integration
 
 import (
 	"bytes"
+	"crypto/sha256"
 	"encoding/base64"
 	"fmt"
 	"net/http"
@@ -24,7 +25,6 @@ import (
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/tests"
 
-	"github.com/minio/sha256-simd"
 	oci "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/stretchr/testify/assert"
 )
diff --git a/tests/integration/api_packages_generic_test.go b/tests/integration/api_packages_generic_test.go
index 93525ac4b1..1cbae599af 100644
--- a/tests/integration/api_packages_generic_test.go
+++ b/tests/integration/api_packages_generic_test.go
@@ -84,7 +84,7 @@ func TestPackageGeneric(t *testing.T) {
 		t.Run("InvalidParameter", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
 
-			req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, "invalid+package name", packageVersion, filename), bytes.NewReader(content)).
+			req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, "invalid package name", packageVersion, filename), bytes.NewReader(content)).
 				AddBasicAuth(user.Name)
 			MakeRequest(t, req, http.StatusBadRequest)
 
@@ -92,7 +92,7 @@ func TestPackageGeneric(t *testing.T) {
 				AddBasicAuth(user.Name)
 			MakeRequest(t, req, http.StatusBadRequest)
 
-			req = NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, packageVersion, "inval+id.na me"), bytes.NewReader(content)).
+			req = NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, packageVersion, "inva|id.name"), bytes.NewReader(content)).
 				AddBasicAuth(user.Name)
 			MakeRequest(t, req, http.StatusBadRequest)
 		})
diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go
index 8c981566b6..daf32e82f9 100644
--- a/tests/integration/api_packages_test.go
+++ b/tests/integration/api_packages_test.go
@@ -5,6 +5,7 @@ package integration
 
 import (
 	"bytes"
+	"crypto/sha256"
 	"fmt"
 	"net/http"
 	"strings"
@@ -24,7 +25,6 @@ import (
 	packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
 	"code.gitea.io/gitea/tests"
 
-	"github.com/minio/sha256-simd"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/tests/integration/api_pull_review_test.go b/tests/integration/api_pull_review_test.go
index daa136b21e..bc544a30b5 100644
--- a/tests/integration/api_pull_review_test.go
+++ b/tests/integration/api_pull_review_test.go
@@ -13,11 +13,14 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/json"
 	api "code.gitea.io/gitea/modules/structs"
+	issue_service "code.gitea.io/gitea/services/issue"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
+	"xorm.io/builder"
 )
 
 func TestAPIPullReview(t *testing.T) {
@@ -276,6 +279,49 @@ func TestAPIPullReviewRequest(t *testing.T) {
 	}).AddTokenAuth(token)
 	MakeRequest(t, req, http.StatusNoContent)
 
+	// a collaborator can add/remove a review request
+	pullIssue21 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 21})
+	assert.NoError(t, pullIssue21.LoadAttributes(db.DefaultContext))
+	pull21Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue21.RepoID}) // repo60
+	user38Session := loginUser(t, "user38")
+	user38Token := getTokenForLoggedInUser(t, user38Session, auth_model.AccessTokenScopeWriteRepository)
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{"user4@example.com"},
+	}).AddTokenAuth(user38Token)
+	MakeRequest(t, req, http.StatusCreated)
+
+	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{"user4@example.com"},
+	}).AddTokenAuth(user38Token)
+	MakeRequest(t, req, http.StatusNoContent)
+
+	// the poster of the PR can add/remove a review request
+	user39Session := loginUser(t, "user39")
+	user39Token := getTokenForLoggedInUser(t, user39Session, auth_model.AccessTokenScopeWriteRepository)
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{"user8"},
+	}).AddTokenAuth(user39Token)
+	MakeRequest(t, req, http.StatusCreated)
+
+	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{"user8"},
+	}).AddTokenAuth(user39Token)
+	MakeRequest(t, req, http.StatusNoContent)
+
+	// user with read permission on pull requests unit can add/remove a review request
+	pullIssue22 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 22})
+	assert.NoError(t, pullIssue22.LoadAttributes(db.DefaultContext))
+	pull22Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue22.RepoID}) // repo61
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull22Repo.OwnerName, pull22Repo.Name, pullIssue22.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{"user38"},
+	}).AddTokenAuth(user39Token) // user39 is from a team with read permission on pull requests unit
+	MakeRequest(t, req, http.StatusCreated)
+
+	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull22Repo.OwnerName, pull22Repo.Name, pullIssue22.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{"user38"},
+	}).AddTokenAuth(user39Token) // user39 is from a team with read permission on pull requests unit
+	MakeRequest(t, req, http.StatusNoContent)
+
 	// Test team review request
 	pullIssue12 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12})
 	assert.NoError(t, pullIssue12.LoadAttributes(db.DefaultContext))
@@ -314,3 +360,126 @@ func TestAPIPullReviewRequest(t *testing.T) {
 		AddTokenAuth(token)
 	MakeRequest(t, req, http.StatusNoContent)
 }
+
+func TestAPIPullReviewStayDismissed(t *testing.T) {
+	// This test against issue https://github.com/go-gitea/gitea/issues/28542
+	// where old reviews surface after a review request got dismissed.
+	defer tests.PrepareTestEnv(t)()
+	pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
+	assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext))
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	session2 := loginUser(t, user2.LoginName)
+	token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository)
+	user8 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 8})
+	session8 := loginUser(t, user8.LoginName)
+	token8 := getTokenForLoggedInUser(t, session8, auth_model.AccessTokenScopeWriteRepository)
+
+	// user2 request user8
+	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{user8.LoginName},
+	}).AddTokenAuth(token2)
+	MakeRequest(t, req, http.StatusCreated)
+
+	reviewsCountCheck(t,
+		"check we have only one review request",
+		pullIssue.ID, user8.ID, 0, 1, 1, false)
+
+	// user2 request user8 again, it is expected to be ignored
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{user8.LoginName},
+	}).AddTokenAuth(token2)
+	MakeRequest(t, req, http.StatusCreated)
+
+	reviewsCountCheck(t,
+		"check we have only one review request, even after re-request it again",
+		pullIssue.ID, user8.ID, 0, 1, 1, false)
+
+	// user8 reviews it as accept
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
+		Event: "APPROVED",
+		Body:  "lgtm",
+	}).AddTokenAuth(token8)
+	MakeRequest(t, req, http.StatusOK)
+
+	reviewsCountCheck(t,
+		"check we have one valid approval",
+		pullIssue.ID, user8.ID, 0, 0, 1, true)
+
+	// emulate of auto-dismiss lgtm on a protected branch that where a pull just got an update
+	_, err := db.GetEngine(db.DefaultContext).Where("issue_id = ? AND reviewer_id = ?", pullIssue.ID, user8.ID).
+		Cols("dismissed").Update(&issues_model.Review{Dismissed: true})
+	assert.NoError(t, err)
+
+	// user2 request user8 again
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
+		Reviewers: []string{user8.LoginName},
+	}).AddTokenAuth(token2)
+	MakeRequest(t, req, http.StatusCreated)
+
+	reviewsCountCheck(t,
+		"check we have no valid approval and one review request",
+		pullIssue.ID, user8.ID, 1, 1, 2, false)
+
+	// user8 dismiss review
+	_, err = issue_service.ReviewRequest(db.DefaultContext, pullIssue, user8, user8, false)
+	assert.NoError(t, err)
+
+	reviewsCountCheck(t,
+		"check new review request is now dismissed",
+		pullIssue.ID, user8.ID, 1, 0, 1, false)
+
+	// add a new valid approval
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
+		Event: "APPROVED",
+		Body:  "lgtm",
+	}).AddTokenAuth(token8)
+	MakeRequest(t, req, http.StatusOK)
+
+	reviewsCountCheck(t,
+		"check that old reviews requests are deleted",
+		pullIssue.ID, user8.ID, 1, 0, 2, true)
+
+	// now add a change request witch should dismiss the approval
+	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
+		Event: "REQUEST_CHANGES",
+		Body:  "please change XYZ",
+	}).AddTokenAuth(token8)
+	MakeRequest(t, req, http.StatusOK)
+
+	reviewsCountCheck(t,
+		"check that old reviews are dismissed",
+		pullIssue.ID, user8.ID, 2, 0, 3, false)
+}
+
+func reviewsCountCheck(t *testing.T, name string, issueID, reviewerID int64, expectedDismissed, expectedRequested, expectedTotal int, expectApproval bool) {
+	t.Run(name, func(t *testing.T) {
+		unittest.AssertCountByCond(t, "review", builder.Eq{
+			"issue_id":    issueID,
+			"reviewer_id": reviewerID,
+			"dismissed":   true,
+		}, expectedDismissed)
+
+		unittest.AssertCountByCond(t, "review", builder.Eq{
+			"issue_id":    issueID,
+			"reviewer_id": reviewerID,
+		}, expectedTotal)
+
+		unittest.AssertCountByCond(t, "review", builder.Eq{
+			"issue_id":    issueID,
+			"reviewer_id": reviewerID,
+			"type":        issues_model.ReviewTypeRequest,
+		}, expectedRequested)
+
+		approvalCount := 0
+		if expectApproval {
+			approvalCount = 1
+		}
+		unittest.AssertCountByCond(t, "review", builder.Eq{
+			"issue_id":    issueID,
+			"reviewer_id": reviewerID,
+			"type":        issues_model.ReviewTypeApprove,
+			"dismissed":   false,
+		}, approvalCount)
+	})
+}
diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go
index 33cc826e5e..bb479caf89 100644
--- a/tests/integration/api_pull_test.go
+++ b/tests/integration/api_pull_test.go
@@ -61,6 +61,27 @@ func TestAPIViewPulls(t *testing.T) {
 	}
 }
 
+func TestAPIViewPullsByBaseHead(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+	ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository)
+
+	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch2", owner.Name, repo.Name).
+		AddTokenAuth(ctx.Token)
+	resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
+
+	pull := &api.PullRequest{}
+	DecodeJSON(t, resp, pull)
+	assert.EqualValues(t, 3, pull.Index)
+	assert.EqualValues(t, 2, pull.ID)
+
+	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch-not-exist", owner.Name, repo.Name).
+		AddTokenAuth(ctx.Token)
+	ctx.Session.MakeRequest(t, req, http.StatusNotFound)
+}
+
 // TestAPIMergePullWIP ensures that we can't merge a WIP pull request
 func TestAPIMergePullWIP(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
@@ -105,6 +126,23 @@ func TestAPICreatePullSuccess(t *testing.T) {
 	MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
 }
 
+func TestAPICreatePullSameRepoSuccess(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+	session := loginUser(t, owner.Name)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner.Name, repo.Name), &api.CreatePullRequestOption{
+		Head:  fmt.Sprintf("%s:pr-to-update", owner.Name),
+		Base:  "master",
+		Title: "successfully create a PR between branches of the same repository",
+	}).AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusCreated)
+	MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
+}
+
 func TestAPICreatePullWithFieldsSuccess(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	// repo10 have code, pulls units.
diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go
index 5b1ab76ce9..49aa4c4e1b 100644
--- a/tests/integration/api_releases_test.go
+++ b/tests/integration/api_releases_test.go
@@ -262,24 +262,60 @@ func TestAPIUploadAssetRelease(t *testing.T) {
 
 	filename := "image.png"
 	buff := generateImg()
-	body := &bytes.Buffer{}
 
-	writer := multipart.NewWriter(body)
-	part, err := writer.CreateFormFile("attachment", filename)
-	assert.NoError(t, err)
-	_, err = io.Copy(part, &buff)
-	assert.NoError(t, err)
-	err = writer.Close()
-	assert.NoError(t, err)
+	assetURL := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner.Name, repo.Name, r.ID)
 
-	req := NewRequestWithBody(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset", owner.Name, repo.Name, r.ID), body).
-		AddTokenAuth(token)
-	req.Header.Add("Content-Type", writer.FormDataContentType())
-	resp := MakeRequest(t, req, http.StatusCreated)
+	t.Run("multipart/form-data", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
 
-	var attachment *api.Attachment
-	DecodeJSON(t, resp, &attachment)
+		body := &bytes.Buffer{}
 
-	assert.EqualValues(t, "test-asset", attachment.Name)
-	assert.EqualValues(t, 104, attachment.Size)
+		writer := multipart.NewWriter(body)
+		part, err := writer.CreateFormFile("attachment", filename)
+		assert.NoError(t, err)
+		_, err = io.Copy(part, bytes.NewReader(buff.Bytes()))
+		assert.NoError(t, err)
+		err = writer.Close()
+		assert.NoError(t, err)
+
+		req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(body.Bytes())).
+			AddTokenAuth(token).
+			SetHeader("Content-Type", writer.FormDataContentType())
+		resp := MakeRequest(t, req, http.StatusCreated)
+
+		var attachment *api.Attachment
+		DecodeJSON(t, resp, &attachment)
+
+		assert.EqualValues(t, filename, attachment.Name)
+		assert.EqualValues(t, 104, attachment.Size)
+
+		req = NewRequestWithBody(t, http.MethodPost, assetURL+"?name=test-asset", bytes.NewReader(body.Bytes())).
+			AddTokenAuth(token).
+			SetHeader("Content-Type", writer.FormDataContentType())
+		resp = MakeRequest(t, req, http.StatusCreated)
+
+		var attachment2 *api.Attachment
+		DecodeJSON(t, resp, &attachment2)
+
+		assert.EqualValues(t, "test-asset", attachment2.Name)
+		assert.EqualValues(t, 104, attachment2.Size)
+	})
+
+	t.Run("application/octet-stream", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(buff.Bytes())).
+			AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusBadRequest)
+
+		req = NewRequestWithBody(t, http.MethodPost, assetURL+"?name=stream.bin", bytes.NewReader(buff.Bytes())).
+			AddTokenAuth(token)
+		resp := MakeRequest(t, req, http.StatusCreated)
+
+		var attachment *api.Attachment
+		DecodeJSON(t, resp, &attachment)
+
+		assert.EqualValues(t, "stream.bin", attachment.Name)
+		assert.EqualValues(t, 104, attachment.Size)
+	})
 }
diff --git a/tests/integration/api_repo_collaborator_test.go b/tests/integration/api_repo_collaborator_test.go
index 59cf85fef3..463db1dfb1 100644
--- a/tests/integration/api_repo_collaborator_test.go
+++ b/tests/integration/api_repo_collaborator_test.go
@@ -27,6 +27,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
 		user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
 		user10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
 		user11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 11})
+		user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
 
 		testCtx := NewAPITestContext(t, repo2Owner.Name, repo2.Name, auth_model.AccessTokenScopeWriteRepository)
 
@@ -86,6 +87,12 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
 			MakeRequest(t, req, http.StatusNotFound)
 		})
 
+		t.Run("CollaboratorBlocked", func(t *testing.T) {
+			ctx := NewAPITestContext(t, repo2Owner.Name, repo2.Name, auth_model.AccessTokenScopeWriteRepository)
+			ctx.ExpectedCode = http.StatusForbidden
+			doAPIAddCollaborator(ctx, user34.Name, perm.AccessModeAdmin)(t)
+		})
+
 		t.Run("CollaboratorCanQueryItsPermissions", func(t *testing.T) {
 			t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user5.Name, perm.AccessModeRead))
 
diff --git a/tests/integration/api_repo_edit_test.go b/tests/integration/api_repo_edit_test.go
index c4fc2177b4..7de8910ee0 100644
--- a/tests/integration/api_repo_edit_test.go
+++ b/tests/integration/api_repo_edit_test.go
@@ -65,6 +65,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
 	allowRebase := false
 	allowRebaseMerge := false
 	allowSquash := false
+	allowFastForwardOnly := false
 	if unit, err := repo.GetUnit(db.DefaultContext, unit_model.TypePullRequests); err == nil {
 		config := unit.PullRequestsConfig()
 		hasPullRequests = true
@@ -73,6 +74,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
 		allowRebase = config.AllowRebase
 		allowRebaseMerge = config.AllowRebaseMerge
 		allowSquash = config.AllowSquash
+		allowFastForwardOnly = config.AllowFastForwardOnly
 	}
 	archived := repo.IsArchived
 	return &api.EditRepoOption{
@@ -92,6 +94,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
 		AllowRebase:               &allowRebase,
 		AllowRebaseMerge:          &allowRebaseMerge,
 		AllowSquash:               &allowSquash,
+		AllowFastForwardOnly:      &allowFastForwardOnly,
 		Archived:                  &archived,
 	}
 }
diff --git a/tests/integration/api_repo_file_create_test.go b/tests/integration/api_repo_file_create_test.go
index 0d192a1fe8..41ad7211ff 100644
--- a/tests/integration/api_repo_file_create_test.go
+++ b/tests/integration/api_repo_file_create_test.go
@@ -17,10 +17,10 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go
index 195a1090c7..ac28e0c0a2 100644
--- a/tests/integration/api_repo_file_update_test.go
+++ b/tests/integration/api_repo_file_update_test.go
@@ -16,10 +16,10 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/tests/integration/api_repo_files_change_test.go b/tests/integration/api_repo_files_change_test.go
index ab5cf19a9c..fb3ae5e4dd 100644
--- a/tests/integration/api_repo_files_change_test.go
+++ b/tests/integration/api_repo_files_change_test.go
@@ -15,10 +15,10 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/context"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go
index 90f84c794e..481732f8df 100644
--- a/tests/integration/api_repo_test.go
+++ b/tests/integration/api_repo_test.go
@@ -93,9 +93,9 @@ func TestAPISearchRepo(t *testing.T) {
 	}{
 		{
 			name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{
-				nil:   {count: 33},
-				user:  {count: 33},
-				user2: {count: 33},
+				nil:   {count: 35},
+				user:  {count: 35},
+				user2: {count: 35},
 			},
 		},
 		{
diff --git a/tests/integration/api_repo_topic_test.go b/tests/integration/api_repo_topic_test.go
index c41bc4abb6..a10e159b78 100644
--- a/tests/integration/api_repo_topic_test.go
+++ b/tests/integration/api_repo_topic_test.go
@@ -26,14 +26,34 @@ func TestAPITopicSearch(t *testing.T) {
 		TopicNames []*api.TopicResponse `json:"topics"`
 	}
 
+	// search all topics
+	res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+	DecodeJSON(t, res, &topics)
+	assert.Len(t, topics.TopicNames, 6)
+	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+	// pagination search topics first page
+	topics.TopicNames = nil
 	query := url.Values{"page": []string{"1"}, "limit": []string{"4"}}
 
 	searchURL.RawQuery = query.Encode()
-	res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
 	DecodeJSON(t, res, &topics)
 	assert.Len(t, topics.TopicNames, 4)
 	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
 
+	// pagination search topics second page
+	topics.TopicNames = nil
+	query = url.Values{"page": []string{"2"}, "limit": []string{"4"}}
+
+	searchURL.RawQuery = query.Encode()
+	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+	DecodeJSON(t, res, &topics)
+	assert.Len(t, topics.TopicNames, 2)
+	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+	// add keyword search
+	query = url.Values{"page": []string{"1"}, "limit": []string{"4"}}
 	query.Add("q", "topic")
 	searchURL.RawQuery = query.Encode()
 	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
diff --git a/tests/integration/api_repo_variables_test.go b/tests/integration/api_repo_variables_test.go
new file mode 100644
index 0000000000..7847962b07
--- /dev/null
+++ b/tests/integration/api_repo_variables_test.go
@@ -0,0 +1,149 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	auth_model "code.gitea.io/gitea/models/auth"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/tests"
+)
+
+func TestAPIRepoVariables(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+	session := loginUser(t, user.Name)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+	t.Run("CreateRepoVariable", func(t *testing.T) {
+		cases := []struct {
+			Name           string
+			ExpectedStatus int
+		}{
+			{
+				Name:           "-",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "_",
+				ExpectedStatus: http.StatusNoContent,
+			},
+			{
+				Name:           "TEST_VAR",
+				ExpectedStatus: http.StatusNoContent,
+			},
+			{
+				Name:           "test_var",
+				ExpectedStatus: http.StatusConflict,
+			},
+			{
+				Name:           "ci",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "123var",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "var@test",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "github_var",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "gitea_var",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+		}
+
+		for _, c := range cases {
+			req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.CreateVariableOption{
+				Value: "value",
+			}).AddTokenAuth(token)
+			MakeRequest(t, req, c.ExpectedStatus)
+		}
+	})
+
+	t.Run("UpdateRepoVariable", func(t *testing.T) {
+		variableName := "test_update_var"
+		url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName)
+		req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
+			Value: "initial_val",
+		}).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		cases := []struct {
+			Name           string
+			UpdateName     string
+			ExpectedStatus int
+		}{
+			{
+				Name:           "not_found_var",
+				ExpectedStatus: http.StatusNotFound,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "1invalid",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "invalid@name",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "ci",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "updated_var_name",
+				ExpectedStatus: http.StatusNoContent,
+			},
+			{
+				Name:           variableName,
+				ExpectedStatus: http.StatusNotFound,
+			},
+			{
+				Name:           "updated_var_name",
+				ExpectedStatus: http.StatusNoContent,
+			},
+		}
+
+		for _, c := range cases {
+			req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.UpdateVariableOption{
+				Name:  c.UpdateName,
+				Value: "updated_val",
+			}).AddTokenAuth(token)
+			MakeRequest(t, req, c.ExpectedStatus)
+		}
+	})
+
+	t.Run("DeleteRepoVariable", func(t *testing.T) {
+		variableName := "test_delete_var"
+		url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName)
+
+		req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
+			Value: "initial_val",
+		}).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNotFound)
+	})
+}
diff --git a/tests/integration/api_user_block_test.go b/tests/integration/api_user_block_test.go
new file mode 100644
index 0000000000..2cc3895a71
--- /dev/null
+++ b/tests/integration/api_user_block_test.go
@@ -0,0 +1,243 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"code.gitea.io/gitea/models"
+	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	repo_model "code.gitea.io/gitea/models/repo"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBlockUser(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	countStars := func(t *testing.T, repoOwnerID, starrerID int64) int64 {
+		count, err := db.Count[repo_model.Repository](db.DefaultContext, &repo_model.StarredReposOptions{
+			StarrerID:      starrerID,
+			RepoOwnerID:    repoOwnerID,
+			IncludePrivate: true,
+		})
+		assert.NoError(t, err)
+		return count
+	}
+
+	countWatches := func(t *testing.T, repoOwnerID, watcherID int64) int64 {
+		count, err := db.Count[repo_model.Repository](db.DefaultContext, &repo_model.WatchedReposOptions{
+			WatcherID:   watcherID,
+			RepoOwnerID: repoOwnerID,
+		})
+		assert.NoError(t, err)
+		return count
+	}
+
+	countRepositoryTransfers := func(t *testing.T, senderID, recipientID int64) int64 {
+		transfers, err := models.GetPendingRepositoryTransfers(db.DefaultContext, &models.PendingRepositoryTransferOptions{
+			SenderID:    senderID,
+			RecipientID: recipientID,
+		})
+		assert.NoError(t, err)
+		return int64(len(transfers))
+	}
+
+	countAssignedIssues := func(t *testing.T, repoOwnerID, assigneeID int64) int64 {
+		_, count, err := issues_model.GetAssignedIssues(db.DefaultContext, &issues_model.AssignedIssuesOptions{
+			AssigneeID:  assigneeID,
+			RepoOwnerID: repoOwnerID,
+		})
+		assert.NoError(t, err)
+		return count
+	}
+
+	countCollaborations := func(t *testing.T, repoOwnerID, collaboratorID int64) int64 {
+		count, err := db.Count[repo_model.Collaboration](db.DefaultContext, &repo_model.FindCollaborationOptions{
+			CollaboratorID: collaboratorID,
+			RepoOwnerID:    repoOwnerID,
+		})
+		assert.NoError(t, err)
+		return count
+	}
+
+	t.Run("User", func(t *testing.T) {
+		var blockerID int64 = 16
+		blockerName := "user16"
+		blockerToken := getUserToken(t, blockerName, auth_model.AccessTokenScopeWriteUser)
+
+		var blockeeID int64 = 10
+		blockeeName := "user10"
+
+		t.Run("Block", func(t *testing.T) {
+			req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName))
+			MakeRequest(t, req, http.StatusUnauthorized)
+
+			assert.EqualValues(t, 1, countStars(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countWatches(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countRepositoryTransfers(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countCollaborations(t, blockerID, blockeeID))
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusNotFound)
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s?reason=test", blockeeName)).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusNoContent)
+
+			assert.EqualValues(t, 0, countStars(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countWatches(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countRepositoryTransfers(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countCollaborations(t, blockerID, blockeeID))
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusNoContent)
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusBadRequest) // can't block blocked user
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", "org3")).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusBadRequest) // can't block organization
+
+			req = NewRequest(t, "GET", "/api/v1/user/blocks")
+			MakeRequest(t, req, http.StatusUnauthorized)
+
+			req = NewRequest(t, "GET", "/api/v1/user/blocks").
+				AddTokenAuth(blockerToken)
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			var users []api.User
+			DecodeJSON(t, resp, &users)
+
+			assert.Len(t, users, 1)
+			assert.Equal(t, blockeeName, users[0].UserName)
+		})
+
+		t.Run("Unblock", func(t *testing.T) {
+			req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName))
+			MakeRequest(t, req, http.StatusUnauthorized)
+
+			req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusNoContent)
+
+			req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusBadRequest)
+
+			req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", "org3")).
+				AddTokenAuth(blockerToken)
+			MakeRequest(t, req, http.StatusBadRequest)
+
+			req = NewRequest(t, "GET", "/api/v1/user/blocks").
+				AddTokenAuth(blockerToken)
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			var users []api.User
+			DecodeJSON(t, resp, &users)
+
+			assert.Empty(t, users)
+		})
+	})
+
+	t.Run("Organization", func(t *testing.T) {
+		var blockerID int64 = 3
+		blockerName := "org3"
+
+		doerToken := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteOrganization)
+
+		var blockeeID int64 = 10
+		blockeeName := "user10"
+
+		t.Run("Block", func(t *testing.T) {
+			req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName))
+			MakeRequest(t, req, http.StatusUnauthorized)
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, "user4")).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusBadRequest) // can't block member
+
+			assert.EqualValues(t, 1, countStars(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countWatches(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countRepositoryTransfers(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countAssignedIssues(t, blockerID, blockeeID))
+			assert.EqualValues(t, 1, countCollaborations(t, blockerID, blockeeID))
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusNotFound)
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s?reason=test", blockerName, blockeeName)).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusNoContent)
+
+			assert.EqualValues(t, 0, countStars(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countWatches(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countRepositoryTransfers(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countAssignedIssues(t, blockerID, blockeeID))
+			assert.EqualValues(t, 0, countCollaborations(t, blockerID, blockeeID))
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusNoContent)
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusBadRequest) // can't block blocked user
+
+			req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, "org3")).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusBadRequest) // can't block organization
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks", blockerName))
+			MakeRequest(t, req, http.StatusUnauthorized)
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks", blockerName)).
+				AddTokenAuth(doerToken)
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			var users []api.User
+			DecodeJSON(t, resp, &users)
+
+			assert.Len(t, users, 1)
+			assert.Equal(t, blockeeName, users[0].UserName)
+		})
+
+		t.Run("Unblock", func(t *testing.T) {
+			req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName))
+			MakeRequest(t, req, http.StatusUnauthorized)
+
+			req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusNoContent)
+
+			req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusBadRequest)
+
+			req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, "org3")).
+				AddTokenAuth(doerToken)
+			MakeRequest(t, req, http.StatusBadRequest)
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks", blockerName)).
+				AddTokenAuth(doerToken)
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			var users []api.User
+			DecodeJSON(t, resp, &users)
+
+			assert.Empty(t, users)
+		})
+	})
+}
diff --git a/tests/integration/api_user_follow_test.go b/tests/integration/api_user_follow_test.go
index 1762732c10..fe20af6769 100644
--- a/tests/integration/api_user_follow_test.go
+++ b/tests/integration/api_user_follow_test.go
@@ -9,6 +9,8 @@ import (
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/tests"
 
@@ -33,6 +35,12 @@ func TestAPIFollow(t *testing.T) {
 		req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/following/%s", user1)).
 			AddTokenAuth(token2)
 		MakeRequest(t, req, http.StatusNoContent)
+
+		// blocked user can't follow blocker
+		user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+		req = NewRequest(t, "PUT", "/api/v1/user/following/user2").
+			AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteUser))
+		MakeRequest(t, req, http.StatusForbidden)
 	})
 
 	t.Run("ListFollowing", func(t *testing.T) {
diff --git a/tests/integration/api_user_star_test.go b/tests/integration/api_user_star_test.go
index 50423c80e7..0062889a92 100644
--- a/tests/integration/api_user_star_test.go
+++ b/tests/integration/api_user_star_test.go
@@ -9,6 +9,8 @@ import (
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/tests"
 
@@ -31,6 +33,12 @@ func TestAPIStar(t *testing.T) {
 		req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s", repo)).
 			AddTokenAuth(tokenWithUserScope)
 		MakeRequest(t, req, http.StatusNoContent)
+
+		// blocked user can't star a repo
+		user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+		req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s", repo)).
+			AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+		MakeRequest(t, req, http.StatusForbidden)
 	})
 
 	t.Run("GetStarredRepos", func(t *testing.T) {
diff --git a/tests/integration/api_user_variables_test.go b/tests/integration/api_user_variables_test.go
new file mode 100644
index 0000000000..dd5501f0b9
--- /dev/null
+++ b/tests/integration/api_user_variables_test.go
@@ -0,0 +1,144 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	auth_model "code.gitea.io/gitea/models/auth"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/tests"
+)
+
+func TestAPIUserVariables(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	session := loginUser(t, "user1")
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
+
+	t.Run("CreateRepoVariable", func(t *testing.T) {
+		cases := []struct {
+			Name           string
+			ExpectedStatus int
+		}{
+			{
+				Name:           "-",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "_",
+				ExpectedStatus: http.StatusNoContent,
+			},
+			{
+				Name:           "TEST_VAR",
+				ExpectedStatus: http.StatusNoContent,
+			},
+			{
+				Name:           "test_var",
+				ExpectedStatus: http.StatusConflict,
+			},
+			{
+				Name:           "ci",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "123var",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "var@test",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "github_var",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           "gitea_var",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+		}
+
+		for _, c := range cases {
+			req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.CreateVariableOption{
+				Value: "value",
+			}).AddTokenAuth(token)
+			MakeRequest(t, req, c.ExpectedStatus)
+		}
+	})
+
+	t.Run("UpdateRepoVariable", func(t *testing.T) {
+		variableName := "test_update_var"
+		url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName)
+		req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
+			Value: "initial_val",
+		}).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		cases := []struct {
+			Name           string
+			UpdateName     string
+			ExpectedStatus int
+		}{
+			{
+				Name:           "not_found_var",
+				ExpectedStatus: http.StatusNotFound,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "1invalid",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "invalid@name",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "ci",
+				ExpectedStatus: http.StatusBadRequest,
+			},
+			{
+				Name:           variableName,
+				UpdateName:     "updated_var_name",
+				ExpectedStatus: http.StatusNoContent,
+			},
+			{
+				Name:           variableName,
+				ExpectedStatus: http.StatusNotFound,
+			},
+			{
+				Name:           "updated_var_name",
+				ExpectedStatus: http.StatusNoContent,
+			},
+		}
+
+		for _, c := range cases {
+			req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.UpdateVariableOption{
+				Name:  c.UpdateName,
+				Value: "updated_val",
+			}).AddTokenAuth(token)
+			MakeRequest(t, req, c.ExpectedStatus)
+		}
+	})
+
+	t.Run("DeleteRepoVariable", func(t *testing.T) {
+		variableName := "test_delete_var"
+		url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName)
+
+		req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
+			Value: "initial_val",
+		}).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNotFound)
+	})
+}
diff --git a/tests/integration/api_user_watch_test.go b/tests/integration/api_user_watch_test.go
index 953e00551d..71dc57453e 100644
--- a/tests/integration/api_user_watch_test.go
+++ b/tests/integration/api_user_watch_test.go
@@ -9,6 +9,8 @@ import (
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/tests"
 
@@ -31,6 +33,12 @@ func TestAPIWatch(t *testing.T) {
 		req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/subscription", repo)).
 			AddTokenAuth(tokenWithRepoScope)
 		MakeRequest(t, req, http.StatusOK)
+
+		// blocked user can't watch a repo
+		user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+		req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/subscription", repo)).
+			AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+		MakeRequest(t, req, http.StatusForbidden)
 	})
 
 	t.Run("GetWatchedRepos", func(t *testing.T) {
diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go
index 1148b3ad39..0d733f663a 100644
--- a/tests/integration/auth_ldap_test.go
+++ b/tests/integration/auth_ldap_test.go
@@ -179,7 +179,7 @@ func TestLDAPUserSignin(t *testing.T) {
 
 	assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name"))
 	assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name"))
-	assert.Equal(t, u.Email, htmlDoc.Find(`label[for="email"]`).Siblings().First().Text())
+	assert.Equal(t, u.Email, htmlDoc.Find("#signed-user-email").Text())
 }
 
 func TestLDAPAuthChange(t *testing.T) {
@@ -309,7 +309,7 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) {
 	// all groups the user is a member of, the user filter is modified accordingly inside
 	// the addAuthSourceLDAP based on the value of the groupFilter
 	u := otherLDAPUsers[0]
-	testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect"))
+	testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect"))
 
 	auth.SyncExternalUsers(context.Background(), true)
 
@@ -362,7 +362,7 @@ func TestLDAPUserSigninFailed(t *testing.T) {
 	addAuthSourceLDAP(t, "", "")
 
 	u := otherLDAPUsers[0]
-	testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect"))
+	testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect"))
 }
 
 func TestLDAPUserSSHKeySync(t *testing.T) {
@@ -428,9 +428,9 @@ func TestLDAPGroupTeamSyncAddMember(t *testing.T) {
 			isMember, err := organization.IsTeamMember(db.DefaultContext, usersOrgs[0].ID, team.ID, user.ID)
 			assert.NoError(t, err)
 			assert.True(t, isMember, "Membership should be added to the right team")
-			err = models.RemoveTeamMember(db.DefaultContext, team, user.ID)
+			err = models.RemoveTeamMember(db.DefaultContext, team, user)
 			assert.NoError(t, err)
-			err = models.RemoveOrgUser(db.DefaultContext, usersOrgs[0].ID, user.ID)
+			err = models.RemoveOrgUser(db.DefaultContext, usersOrgs[0], user)
 			assert.NoError(t, err)
 		} else {
 			// assert members of LDAP group "cn=admin_staff" keep initial team membership since mapped team does not exist
@@ -460,7 +460,7 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
 	})
 	err = organization.AddOrgUser(db.DefaultContext, org.ID, user.ID)
 	assert.NoError(t, err)
-	err = models.AddTeamMember(db.DefaultContext, team, user.ID)
+	err = models.AddTeamMember(db.DefaultContext, team, user)
 	assert.NoError(t, err)
 	isMember, err := organization.IsOrganizationMember(db.DefaultContext, org.ID, user.ID)
 	assert.NoError(t, err)
diff --git a/tests/integration/branches_test.go b/tests/integration/branches_test.go
index 99d7eef706..e148fe2d6f 100644
--- a/tests/integration/branches_test.go
+++ b/tests/integration/branches_test.go
@@ -37,7 +37,7 @@ func TestUndoDeleteBranch(t *testing.T) {
 		htmlDoc, name := branchAction(t, ".restore-branch-button")
 		assert.Contains(t,
 			htmlDoc.doc.Find(".ui.positive.message").Text(),
-			translation.NewLocale("en-US").Tr("repo.branch.restore_success", name),
+			translation.NewLocale("en-US").TrString("repo.branch.restore_success", name),
 		)
 	})
 }
@@ -46,7 +46,7 @@ func deleteBranch(t *testing.T) {
 	htmlDoc, name := branchAction(t, ".delete-branch-button")
 	assert.Contains(t,
 		htmlDoc.doc.Find(".ui.positive.message").Text(),
-		translation.NewLocale("en-US").Tr("repo.branch.deletion_success", name),
+		translation.NewLocale("en-US").TrString("repo.branch.deletion_success", name),
 	)
 }
 
diff --git a/tests/integration/db_collation_test.go b/tests/integration/db_collation_test.go
index 468d13508d..75a4c1594f 100644
--- a/tests/integration/db_collation_test.go
+++ b/tests/integration/db_collation_test.go
@@ -22,12 +22,6 @@ type TestCollationTbl struct {
 func TestDatabaseCollation(t *testing.T) {
 	x := db.GetEngine(db.DefaultContext).(*xorm.Engine)
 
-	// there are blockers for MSSQL to use case-sensitive collation, see the comments in db/collation.go
-	if setting.Database.Type.IsMSSQL() {
-		t.Skip("there are blockers for MSSQL to use case-sensitive collation")
-		return
-	}
-
 	// all created tables should use case-sensitive collation by default
 	_, _ = x.Exec("DROP TABLE IF EXISTS test_collation_tbl")
 	err := x.Sync(&TestCollationTbl{})
diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go
index de2a9d7d23..045567ce77 100644
--- a/tests/integration/editor_test.go
+++ b/tests/integration/editor_test.go
@@ -10,8 +10,8 @@ import (
 	"path"
 	"testing"
 
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/json"
+	gitea_context "code.gitea.io/gitea/services/context"
 
 	"github.com/stretchr/testify/assert"
 )
diff --git a/tests/integration/explore_repos_test.go b/tests/integration/explore_repos_test.go
index 26fd1dde64..1e3ab314fd 100644
--- a/tests/integration/explore_repos_test.go
+++ b/tests/integration/explore_repos_test.go
@@ -8,11 +8,18 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestExploreRepos(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
-	req := NewRequest(t, "GET", "/explore/repos")
-	MakeRequest(t, req, http.StatusOK)
+	req := NewRequest(t, "GET", "/explore/repos?q=TheKeyword&topic=1&language=TheLang")
+	resp := MakeRequest(t, req, http.StatusOK)
+	respStr := resp.Body.String()
+
+	assert.Contains(t, respStr, `<input type="hidden" name="topic" value="true">`)
+	assert.Contains(t, respStr, `<input type="hidden" name="language" value="TheLang">`)
+	assert.Contains(t, respStr, `<input type="search" name="q" value="TheKeyword"`)
 }
diff --git a/tests/integration/explore_user_test.go b/tests/integration/explore_user_test.go
new file mode 100644
index 0000000000..441d89cea5
--- /dev/null
+++ b/tests/integration/explore_user_test.go
@@ -0,0 +1,44 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"net/http"
+	"testing"
+
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestExploreUser(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	cases := []struct{ sortOrder, expected string }{
+		{"", "?sort=newest&q="},
+		{"newest", "?sort=newest&q="},
+		{"oldest", "?sort=oldest&q="},
+		{"alphabetically", "?sort=alphabetically&q="},
+		{"reversealphabetically", "?sort=reversealphabetically&q="},
+	}
+	for _, c := range cases {
+		req := NewRequest(t, "GET", "/explore/users?sort="+c.sortOrder)
+		resp := MakeRequest(t, req, http.StatusOK)
+		h := NewHTMLParser(t, resp.Body)
+		href, _ := h.Find(`.ui.dropdown .menu a.active.item[href^="?sort="]`).Attr("href")
+		assert.Equal(t, c.expected, href)
+	}
+
+	// these sort orders shouldn't be supported, to avoid leaking user activity
+	cases404 := []string{
+		"/explore/users?sort=lastlogin",
+		"/explore/users?sort=reverselastlogin",
+		"/explore/users?sort=leastupdate",
+		"/explore/users?sort=reverseleastupdate",
+	}
+	for _, c := range cases404 {
+		req := NewRequest(t, "GET", c).SetHeader("Accept", "text/html")
+		MakeRequest(t, req, http.StatusNotFound)
+	}
+}
diff --git a/tests/integration/git_clone_wiki_test.go b/tests/integration/git_clone_wiki_test.go
index d7949dfe25..ef662300f3 100644
--- a/tests/integration/git_clone_wiki_test.go
+++ b/tests/integration/git_clone_wiki_test.go
@@ -45,6 +45,7 @@ func TestRepoCloneWiki(t *testing.T) {
 			assertFileExist(t, filepath.Join(dstPath, "Page-With-Image.md"))
 			assertFileExist(t, filepath.Join(dstPath, "Page-With-Spaced-Name.md"))
 			assertFileExist(t, filepath.Join(dstPath, "images"))
+			assertFileExist(t, filepath.Join(dstPath, "files/Non-Renderable-File.zip"))
 			assertFileExist(t, filepath.Join(dstPath, "jpeg.jpg"))
 		})
 	})
diff --git a/tests/integration/git_push_test.go b/tests/integration/git_push_test.go
new file mode 100644
index 0000000000..b37fb02444
--- /dev/null
+++ b/tests/integration/git_push_test.go
@@ -0,0 +1,159 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"fmt"
+	"net/url"
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	repo_service "code.gitea.io/gitea/services/repository"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGitPush(t *testing.T) {
+	onGiteaRun(t, testGitPush)
+}
+
+func testGitPush(t *testing.T, u *url.URL) {
+	t.Run("Push branches at once", func(t *testing.T) {
+		runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
+			for i := 0; i < 100; i++ {
+				branchName := fmt.Sprintf("branch-%d", i)
+				pushed = append(pushed, branchName)
+				doGitCreateBranch(gitPath, branchName)(t)
+			}
+			pushed = append(pushed, "master")
+			doGitPushTestRepository(gitPath, "origin", "--all")(t)
+			return pushed, deleted
+		})
+	})
+
+	t.Run("Push branches one by one", func(t *testing.T) {
+		runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
+			for i := 0; i < 100; i++ {
+				branchName := fmt.Sprintf("branch-%d", i)
+				doGitCreateBranch(gitPath, branchName)(t)
+				doGitPushTestRepository(gitPath, "origin", branchName)(t)
+				pushed = append(pushed, branchName)
+			}
+			return pushed, deleted
+		})
+	})
+
+	t.Run("Push branch with options", func(t *testing.T) {
+		runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
+			branchName := "branch-with-options"
+			doGitCreateBranch(gitPath, branchName)(t)
+			doGitPushTestRepository(gitPath, "origin", branchName, "-o", "repo.private=true", "-o", "repo.template=true")(t)
+			pushed = append(pushed, branchName)
+
+			return pushed, deleted
+		})
+	})
+
+	t.Run("Delete branches", func(t *testing.T) {
+		runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
+			doGitPushTestRepository(gitPath, "origin", "master")(t) // make sure master is the default branch instead of a branch we are going to delete
+			pushed = append(pushed, "master")
+
+			for i := 0; i < 100; i++ {
+				branchName := fmt.Sprintf("branch-%d", i)
+				pushed = append(pushed, branchName)
+				doGitCreateBranch(gitPath, branchName)(t)
+			}
+			doGitPushTestRepository(gitPath, "origin", "--all")(t)
+
+			for i := 0; i < 10; i++ {
+				branchName := fmt.Sprintf("branch-%d", i)
+				doGitPushTestRepository(gitPath, "origin", "--delete", branchName)(t)
+				deleted = append(deleted, branchName)
+			}
+			return pushed, deleted
+		})
+	})
+
+	t.Run("Push to deleted branch", func(t *testing.T) {
+		runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
+			doGitPushTestRepository(gitPath, "origin", "master")(t) // make sure master is the default branch instead of a branch we are going to delete
+			pushed = append(pushed, "master")
+
+			doGitCreateBranch(gitPath, "branch-1")(t)
+			doGitPushTestRepository(gitPath, "origin", "branch-1")(t)
+			pushed = append(pushed, "branch-1")
+
+			// delete and restore
+			doGitPushTestRepository(gitPath, "origin", "--delete", "branch-1")(t)
+			doGitPushTestRepository(gitPath, "origin", "branch-1")(t)
+
+			return pushed, deleted
+		})
+	})
+}
+
+func runTestGitPush(t *testing.T, u *url.URL, gitOperation func(t *testing.T, gitPath string) (pushed, deleted []string)) {
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
+		Name:          "repo-to-push",
+		Description:   "test git push",
+		AutoInit:      false,
+		DefaultBranch: "main",
+		IsPrivate:     false,
+	})
+	require.NoError(t, err)
+	require.NotEmpty(t, repo)
+
+	gitPath := t.TempDir()
+
+	doGitInitTestRepository(gitPath)(t)
+
+	oldPath := u.Path
+	oldUser := u.User
+	defer func() {
+		u.Path = oldPath
+		u.User = oldUser
+	}()
+	u.Path = repo.FullName() + ".git"
+	u.User = url.UserPassword(user.LowerName, userPassword)
+
+	doGitAddRemote(gitPath, "origin", u)(t)
+
+	gitRepo, err := git.OpenRepository(git.DefaultContext, gitPath)
+	require.NoError(t, err)
+	defer gitRepo.Close()
+
+	pushedBranches, deletedBranches := gitOperation(t, gitPath)
+
+	dbBranches := make([]*git_model.Branch, 0)
+	require.NoError(t, db.GetEngine(db.DefaultContext).Where("repo_id=?", repo.ID).Find(&dbBranches))
+	assert.Equalf(t, len(pushedBranches), len(dbBranches), "mismatched number of branches in db")
+	dbBranchesMap := make(map[string]*git_model.Branch, len(dbBranches))
+	for _, branch := range dbBranches {
+		dbBranchesMap[branch.Name] = branch
+	}
+
+	deletedBranchesMap := make(map[string]bool, len(deletedBranches))
+	for _, branchName := range deletedBranches {
+		deletedBranchesMap[branchName] = true
+	}
+
+	for _, branchName := range pushedBranches {
+		branch, ok := dbBranchesMap[branchName]
+		deleted := deletedBranchesMap[branchName]
+		assert.True(t, ok, "branch %s not found in database", branchName)
+		assert.Equal(t, deleted, branch.IsDeleted, "IsDeleted of %s is %v, but it's expected to be %v", branchName, branch.IsDeleted, deleted)
+		commitID, err := gitRepo.GetBranchCommitID(branchName)
+		require.NoError(t, err)
+		assert.Equal(t, commitID, branch.CommitID)
+	}
+
+	require.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, repo.ID))
+}
diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go
index 0c3a8616f0..818e1fa653 100644
--- a/tests/integration/git_test.go
+++ b/tests/integration/git_test.go
@@ -4,6 +4,7 @@
 package integration
 
 import (
+	"bytes"
 	"encoding/hex"
 	"fmt"
 	"math/rand"
@@ -23,11 +24,13 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	gitea_context "code.gitea.io/gitea/services/context"
+	files_service "code.gitea.io/gitea/services/repository/files"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
@@ -848,3 +851,44 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB
 		t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master"))
 	}
 }
+
+func TestDataAsync_Issue29101(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+		resp, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{
+			Files: []*files_service.ChangeRepoFile{
+				{
+					Operation:     "create",
+					TreePath:      "test.txt",
+					ContentReader: bytes.NewReader(make([]byte, 10000)),
+				},
+			},
+			OldBranch: repo.DefaultBranch,
+			NewBranch: repo.DefaultBranch,
+		})
+		assert.NoError(t, err)
+
+		sha := resp.Commit.SHA
+
+		gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo)
+		assert.NoError(t, err)
+
+		commit, err := gitRepo.GetCommit(sha)
+		assert.NoError(t, err)
+
+		entry, err := commit.GetTreeEntryByPath("test.txt")
+		assert.NoError(t, err)
+
+		b := entry.Blob()
+
+		r, err := b.DataAsync()
+		assert.NoError(t, err)
+		defer r.Close()
+
+		r2, err := b.DataAsync()
+		assert.NoError(t, err)
+		defer r2.Close()
+	})
+}
diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go
index 1127de1afc..f9bd352b62 100644
--- a/tests/integration/integration_test.go
+++ b/tests/integration/integration_test.go
@@ -24,7 +24,6 @@ import (
 
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/unittest"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
@@ -33,6 +32,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers"
+	gitea_context "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/PuerkitoBio/goquery"
diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go
index 4b3f581c2b..44d362d9c7 100644
--- a/tests/integration/issue_test.go
+++ b/tests/integration/issue_test.go
@@ -407,7 +407,7 @@ func TestSearchIssues(t *testing.T) {
 
 	session := loginUser(t, "user2")
 
-	expectedIssueCount := 18 // from the fixtures
+	expectedIssueCount := 20 // from the fixtures
 	if expectedIssueCount > setting.UI.IssuePagingNum {
 		expectedIssueCount = setting.UI.IssuePagingNum
 	}
@@ -444,7 +444,7 @@ func TestSearchIssues(t *testing.T) {
 	req = NewRequest(t, "GET", link.String())
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &apiIssues)
-	assert.EqualValues(t, "20", resp.Header().Get("X-Total-Count"))
+	assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count"))
 	assert.Len(t, apiIssues, 20)
 
 	query.Add("limit", "5")
@@ -452,7 +452,7 @@ func TestSearchIssues(t *testing.T) {
 	req = NewRequest(t, "GET", link.String())
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &apiIssues)
-	assert.EqualValues(t, "20", resp.Header().Get("X-Total-Count"))
+	assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count"))
 	assert.Len(t, apiIssues, 5)
 
 	query = url.Values{"assigned": {"true"}, "state": {"all"}}
@@ -501,7 +501,7 @@ func TestSearchIssues(t *testing.T) {
 func TestSearchIssuesWithLabels(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
-	expectedIssueCount := 18 // from the fixtures
+	expectedIssueCount := 20 // from the fixtures
 	if expectedIssueCount > setting.UI.IssuePagingNum {
 		expectedIssueCount = setting.UI.IssuePagingNum
 	}
diff --git a/tests/integration/linguist_test.go b/tests/integration/linguist_test.go
new file mode 100644
index 0000000000..e569de93a8
--- /dev/null
+++ b/tests/integration/linguist_test.go
@@ -0,0 +1,259 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"context"
+	"net/url"
+	"strings"
+	"testing"
+	"time"
+
+	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/indexer/stats"
+	"code.gitea.io/gitea/modules/queue"
+	repo_service "code.gitea.io/gitea/services/repository"
+	files_service "code.gitea.io/gitea/services/repository/files"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLinguist(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, _ *url.URL) {
+		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+		cppContent := "#include <iostream>\nint main() {\nstd::cout << \"Hello Gitea!\";\nreturn 0;\n}"
+		pyContent := "print(\"Hello Gitea!\")"
+		phpContent := "<?php\necho 'Hallo Welt';\n?>"
+		lockContent := "# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand."
+		mdContent := "markdown"
+
+		cases := []struct {
+			GitAttributesContent  string
+			FilesToAdd            []*files_service.ChangeRepoFile
+			ExpectedLanguageOrder []string
+		}{
+			// case 0
+			{
+				ExpectedLanguageOrder: []string{},
+			},
+			// case 1
+			{
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+					{
+						TreePath:      "python.py",
+						ContentReader: strings.NewReader(pyContent),
+					},
+					{
+						TreePath:      "php.php",
+						ContentReader: strings.NewReader(phpContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"C++", "PHP", "Python"},
+			},
+			// case 2
+			{
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      ".cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+					{
+						TreePath:      "python.py",
+						ContentReader: strings.NewReader(pyContent),
+					},
+					{
+						TreePath:      "vendor/php.php",
+						ContentReader: strings.NewReader(phpContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"Python"},
+			},
+			// case 3
+			{
+				GitAttributesContent: "*.cpp linguist-language=Go",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"Go"},
+			},
+			// case 4
+			{
+				GitAttributesContent: "*.cpp gitlab-language=Go?parent=json",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"Go"},
+			},
+			// case 5
+			{
+				GitAttributesContent: "*.cpp linguist-language=HTML gitlab-language=Go?parent=json",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"HTML"},
+			},
+			// case 6
+			{
+				GitAttributesContent: "vendor/** linguist-vendored=false",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "vendor/php.php",
+						ContentReader: strings.NewReader(phpContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"PHP"},
+			},
+			// case 7
+			{
+				GitAttributesContent: "*.cpp linguist-vendored=true\n*.py linguist-vendored\nvendor/** -linguist-vendored",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+					{
+						TreePath:      "python.py",
+						ContentReader: strings.NewReader(pyContent),
+					},
+					{
+						TreePath:      "vendor/php.php",
+						ContentReader: strings.NewReader(phpContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"PHP"},
+			},
+			// case 8
+			{
+				GitAttributesContent: "poetry.lock linguist-language=Go",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "poetry.lock",
+						ContentReader: strings.NewReader(lockContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"Go"},
+			},
+			// case 9
+			{
+				GitAttributesContent: "poetry.lock linguist-generated=false",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "poetry.lock",
+						ContentReader: strings.NewReader(lockContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"TOML"},
+			},
+			// case 10
+			{
+				GitAttributesContent: "*.cpp -linguist-detectable",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{},
+			},
+			// case 11
+			{
+				GitAttributesContent: "*.md linguist-detectable",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "test.md",
+						ContentReader: strings.NewReader(mdContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"Markdown"},
+			},
+			// case 12
+			{
+				GitAttributesContent: "test2.md linguist-detectable",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "cplusplus.cpp",
+						ContentReader: strings.NewReader(cppContent),
+					},
+					{
+						TreePath:      "test.md",
+						ContentReader: strings.NewReader(mdContent),
+					},
+					{
+						TreePath:      "test2.md",
+						ContentReader: strings.NewReader(mdContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"C++", "Markdown"},
+			},
+			// case 13
+			{
+				GitAttributesContent: "README.md linguist-documentation=false",
+				FilesToAdd: []*files_service.ChangeRepoFile{
+					{
+						TreePath:      "README.md",
+						ContentReader: strings.NewReader(mdContent),
+					},
+				},
+				ExpectedLanguageOrder: []string{"Markdown"},
+			},
+		}
+
+		for i, c := range cases {
+			repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
+				Name: "linguist-test",
+			})
+			assert.NoError(t, err)
+
+			files := []*files_service.ChangeRepoFile{
+				{
+					TreePath:      ".gitattributes",
+					ContentReader: strings.NewReader(c.GitAttributesContent),
+				},
+			}
+			files = append(files, c.FilesToAdd...)
+			for _, f := range files {
+				f.Operation = "create"
+			}
+
+			_, err = files_service.ChangeRepoFiles(git.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{
+				Files:     files,
+				OldBranch: repo.DefaultBranch,
+				NewBranch: repo.DefaultBranch,
+			})
+			assert.NoError(t, err)
+
+			assert.NoError(t, stats.UpdateRepoIndexer(repo))
+			assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 10*time.Second))
+
+			stats, err := repo_model.GetTopLanguageStats(db.DefaultContext, repo, len(c.FilesToAdd))
+			assert.NoError(t, err)
+
+			languages := make([]string, 0, len(stats))
+			for _, s := range stats {
+				languages = append(languages, s.Language)
+			}
+			assert.Equal(t, c.ExpectedLanguageOrder, languages, "case %d: unexpected language stats", i)
+
+			assert.NoError(t, repo_service.DeleteRepository(db.DefaultContext, user, repo, false))
+		}
+	})
+}
diff --git a/tests/integration/links_test.go b/tests/integration/links_test.go
index a3937dd697..d103e2b0a9 100644
--- a/tests/integration/links_test.go
+++ b/tests/integration/links_test.go
@@ -36,6 +36,7 @@ func TestLinksNoLogin(t *testing.T) {
 		"/user2/repo1/",
 		"/user2/repo1/projects",
 		"/user2/repo1/projects/1",
+		"/user2/repo1/releases/tag/delete-tag", // It's the only one existing record on release.yml which has is_tag: true
 		"/assets/img/404.png",
 		"/assets/img/500.png",
 		"/.well-known/security.txt",
diff --git a/tests/integration/mirror_pull_test.go b/tests/integration/mirror_pull_test.go
index 2e71b80fbb..77050c4bbc 100644
--- a/tests/integration/mirror_pull_test.go
+++ b/tests/integration/mirror_pull_test.go
@@ -14,7 +14,6 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/migration"
-	"code.gitea.io/gitea/modules/repository"
 	mirror_service "code.gitea.io/gitea/services/mirror"
 	release_service "code.gitea.io/gitea/services/release"
 	repo_service "code.gitea.io/gitea/services/repository"
@@ -52,7 +51,7 @@ func TestMirrorPull(t *testing.T) {
 
 	ctx := context.Background()
 
-	mirror, err := repository.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
+	mirror, err := repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
 	assert.NoError(t, err)
 
 	gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go
index 3dc719593c..1c262b3349 100644
--- a/tests/integration/mirror_push_test.go
+++ b/tests/integration/mirror_push_test.go
@@ -15,10 +15,10 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
+	gitea_context "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/migrations"
 	mirror_service "code.gitea.io/gitea/services/mirror"
 	repo_service "code.gitea.io/gitea/services/repository"
diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go
index f5baf05965..5ce8ea3031 100644
--- a/tests/integration/pull_compare_test.go
+++ b/tests/integration/pull_compare_test.go
@@ -25,4 +25,11 @@ func TestPullCompare(t *testing.T) {
 	req = NewRequest(t, "GET", link)
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	assert.EqualValues(t, http.StatusOK, resp.Code)
+
+	// test the edit button in the PR diff view
+	req = NewRequest(t, "GET", "/user2/repo1/pulls/3/files")
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	doc := NewHTMLParser(t, resp.Body)
+	editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length()
+	assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none")
 }
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
index fcd7fecd52..daf411f452 100644
--- a/tests/integration/pull_merge_test.go
+++ b/tests/integration/pull_merge_test.go
@@ -219,7 +219,7 @@ func TestCantMergeWorkInProgress(t *testing.T) {
 		text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text())
 		assert.NotEmpty(t, text, "Can't find WIP text")
 
-		assert.Contains(t, text, translation.NewLocale("en-US").Tr("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
+		assert.Contains(t, text, translation.NewLocale("en-US").TrString("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
 		assert.Contains(t, text, "[wip]", "Unable to find WIP text")
 	})
 }
@@ -365,6 +365,90 @@ func TestCantMergeUnrelated(t *testing.T) {
 	})
 }
 
+func TestFastForwardOnlyMerge(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		session := loginUser(t, "user1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "update", "README.md", "Hello, World 2\n")
+
+		// Use API to create a pr from update to master
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
+			Head:  "update",
+			Base:  "master",
+			Title: "create a pr that can be fast-forward-only merged",
+		}).AddTokenAuth(token)
+		session.MakeRequest(t, req, http.StatusCreated)
+
+		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
+			Name: "user1",
+		})
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
+			OwnerID: user1.ID,
+			Name:    "repo1",
+		})
+
+		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
+			HeadRepoID: repo1.ID,
+			BaseRepoID: repo1.ID,
+			HeadBranch: "update",
+			BaseBranch: "master",
+		})
+
+		gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
+		assert.NoError(t, err)
+
+		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false)
+
+		assert.NoError(t, err)
+
+		gitRepo.Close()
+	})
+}
+
+func TestCantFastForwardOnlyMergeDiverging(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		session := loginUser(t, "user1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "diverging", "README.md", "Hello, World diverged\n")
+		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World 2\n")
+
+		// Use API to create a pr from diverging to update
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
+			Head:  "diverging",
+			Base:  "master",
+			Title: "create a pr from a diverging branch",
+		}).AddTokenAuth(token)
+		session.MakeRequest(t, req, http.StatusCreated)
+
+		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
+			Name: "user1",
+		})
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
+			OwnerID: user1.ID,
+			Name:    "repo1",
+		})
+
+		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
+			HeadRepoID: repo1.ID,
+			BaseRepoID: repo1.ID,
+			HeadBranch: "diverging",
+			BaseBranch: "master",
+		})
+
+		gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
+		assert.NoError(t, err)
+
+		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "DIVERGING", false)
+
+		assert.Error(t, err, "Merge should return an error due to being for a diverging branch")
+		assert.True(t, models.IsErrMergeDivergingFastForwardOnly(err), "Merge error is not a diverging fast-forward-only error")
+
+		gitRepo.Close()
+	})
+}
+
 func TestConflictChecking(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@@ -432,8 +516,8 @@ func TestConflictChecking(t *testing.T) {
 		assert.NoError(t, err)
 
 		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
-		conflictingPR, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID)
-		assert.NoError(t, err)
+		assert.NoError(t, issue.LoadPullRequest(db.DefaultContext))
+		conflictingPR := issue.PullRequest
 
 		// Ensure conflictedFiles is populated.
 		assert.Len(t, conflictingPR.ConflictedFiles, 1)
diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
index 68d80a1021..9a5877697c 100644
--- a/tests/integration/pull_review_test.go
+++ b/tests/integration/pull_review_test.go
@@ -5,9 +5,22 @@ package integration
 
 import (
 	"net/http"
+	"net/url"
+	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/test"
+	issue_service "code.gitea.io/gitea/services/issue"
+	repo_service "code.gitea.io/gitea/services/repository"
+	files_service "code.gitea.io/gitea/services/repository/files"
 	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestPullView_ReviewerMissed(t *testing.T) {
@@ -15,8 +28,143 @@ func TestPullView_ReviewerMissed(t *testing.T) {
 	session := loginUser(t, "user1")
 
 	req := NewRequest(t, "GET", "/pulls")
-	session.MakeRequest(t, req, http.StatusOK)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
 
 	req = NewRequest(t, "GET", "/user2/repo1/pulls/3")
-	session.MakeRequest(t, req, http.StatusOK)
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
+
+	// if some reviews are missing, the page shouldn't fail
+	err := db.TruncateBeans(db.DefaultContext, &issues_model.Review{})
+	assert.NoError(t, err)
+	req = NewRequest(t, "GET", "/user2/repo1/pulls/2")
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
+}
+
+func TestPullView_CodeOwner(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+		// Create the repo.
+		repo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
+			Name:             "test_codeowner",
+			Readme:           "Default",
+			AutoInit:         true,
+			ObjectFormatName: git.Sha1ObjectFormat.Name(),
+			DefaultBranch:    "master",
+		})
+		assert.NoError(t, err)
+
+		// add CODEOWNERS to default branch
+		_, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+			OldBranch: repo.DefaultBranch,
+			Files: []*files_service.ChangeRepoFile{
+				{
+					Operation:     "create",
+					TreePath:      "CODEOWNERS",
+					ContentReader: strings.NewReader("README.md @user5\n"),
+				},
+			},
+		})
+		assert.NoError(t, err)
+
+		t.Run("First Pull Request", func(t *testing.T) {
+			// create a new branch to prepare for pull request
+			_, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+				NewBranch: "codeowner-basebranch",
+				Files: []*files_service.ChangeRepoFile{
+					{
+						Operation:     "update",
+						TreePath:      "README.md",
+						ContentReader: strings.NewReader("# This is a new project\n"),
+					},
+				},
+			})
+			assert.NoError(t, err)
+
+			// Create a pull request.
+			session := loginUser(t, "user2")
+			testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch", "Test Pull Request")
+
+			pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "codeowner-basebranch"})
+			unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5})
+			assert.NoError(t, pr.LoadIssue(db.DefaultContext))
+
+			err := issue_service.ChangeTitle(db.DefaultContext, pr.Issue, user2, "[WIP] Test Pull Request")
+			assert.NoError(t, err)
+			prUpdated1 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
+			assert.NoError(t, prUpdated1.LoadIssue(db.DefaultContext))
+			assert.EqualValues(t, "[WIP] Test Pull Request", prUpdated1.Issue.Title)
+
+			err = issue_service.ChangeTitle(db.DefaultContext, prUpdated1.Issue, user2, "Test Pull Request2")
+			assert.NoError(t, err)
+			prUpdated2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
+			assert.NoError(t, prUpdated2.LoadIssue(db.DefaultContext))
+			assert.EqualValues(t, "Test Pull Request2", prUpdated2.Issue.Title)
+		})
+
+		// change the default branch CODEOWNERS file to change README.md's codeowner
+		_, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+			Files: []*files_service.ChangeRepoFile{
+				{
+					Operation:     "update",
+					TreePath:      "CODEOWNERS",
+					ContentReader: strings.NewReader("README.md @user8\n"),
+				},
+			},
+		})
+		assert.NoError(t, err)
+
+		t.Run("Second Pull Request", func(t *testing.T) {
+			// create a new branch to prepare for pull request
+			_, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+				NewBranch: "codeowner-basebranch2",
+				Files: []*files_service.ChangeRepoFile{
+					{
+						Operation:     "update",
+						TreePath:      "README.md",
+						ContentReader: strings.NewReader("# This is a new project2\n"),
+					},
+				},
+			})
+			assert.NoError(t, err)
+
+			// Create a pull request.
+			session := loginUser(t, "user2")
+			testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch2", "Test Pull Request2")
+
+			pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch2"})
+			unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
+		})
+
+		t.Run("Forked Repo Pull Request", func(t *testing.T) {
+			user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+			forkedRepo, err := repo_service.ForkRepository(db.DefaultContext, user2, user5, repo_service.ForkRepoOptions{
+				BaseRepo: repo,
+				Name:     "test_codeowner",
+			})
+			assert.NoError(t, err)
+
+			// create a new branch to prepare for pull request
+			_, err = files_service.ChangeRepoFiles(db.DefaultContext, forkedRepo, user5, &files_service.ChangeRepoFilesOptions{
+				NewBranch: "codeowner-basebranch-forked",
+				Files: []*files_service.ChangeRepoFile{
+					{
+						Operation:     "update",
+						TreePath:      "README.md",
+						ContentReader: strings.NewReader("# This is a new forked project\n"),
+					},
+				},
+			})
+			assert.NoError(t, err)
+
+			session := loginUser(t, "user5")
+			testPullCreate(t, session, "user5", "test_codeowner", true, forkedRepo.DefaultBranch, "codeowner-basebranch-forked", "Test Pull Request2")
+
+			pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: forkedRepo.ID, HeadBranch: "codeowner-basebranch-forked"})
+			unittest.AssertExistsIf(t, false, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
+		})
+	})
 }
diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go
index 26c99e6445..bb7098e424 100644
--- a/tests/integration/pull_status_test.go
+++ b/tests/integration/pull_status_test.go
@@ -12,6 +12,9 @@ import (
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
+	git_model "code.gitea.io/gitea/models/git"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
 	api "code.gitea.io/gitea/modules/structs"
 
 	"github.com/stretchr/testify/assert"
@@ -90,6 +93,10 @@ func TestPullCreate_CommitStatus(t *testing.T) {
 			assert.True(t, ok)
 			assert.Contains(t, cls, statesIcons[status])
 		}
+
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"})
+		css := unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusSummary{RepoID: repo1.ID, SHA: commitID})
+		assert.EqualValues(t, api.CommitStatusWarning, css.State)
 	})
 }
 
diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go
index 078253ffb0..5ae241f3af 100644
--- a/tests/integration/pull_update_test.go
+++ b/tests/integration/pull_update_test.go
@@ -177,8 +177,7 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
 	assert.NoError(t, err)
 
 	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"})
-	pr, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID)
-	assert.NoError(t, err)
+	assert.NoError(t, issue.LoadPullRequest(db.DefaultContext))
 
-	return pr
+	return issue.PullRequest
 }
diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go
index 42d0d00e78..ce0c440167 100644
--- a/tests/integration/release_test.go
+++ b/tests/integration/release_test.go
@@ -86,7 +86,7 @@ func TestCreateRelease(t *testing.T) {
 	session := loginUser(t, "user2")
 	createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, false)
 
-	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4)
+	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.stable"), 4)
 }
 
 func TestCreateReleasePreRelease(t *testing.T) {
@@ -95,7 +95,7 @@ func TestCreateReleasePreRelease(t *testing.T) {
 	session := loginUser(t, "user2")
 	createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", true, false)
 
-	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.prerelease"), 4)
+	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.prerelease"), 4)
 }
 
 func TestCreateReleaseDraft(t *testing.T) {
@@ -104,7 +104,7 @@ func TestCreateReleaseDraft(t *testing.T) {
 	session := loginUser(t, "user2")
 	createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, true)
 
-	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.draft"), 4)
+	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.draft"), 4)
 }
 
 func TestCreateReleasePaging(t *testing.T) {
@@ -124,11 +124,11 @@ func TestCreateReleasePaging(t *testing.T) {
 	}
 	createNewRelease(t, session, "/user2/repo1", "v0.0.12", "v0.0.12", false, true)
 
-	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.12", translation.NewLocale("en-US").Tr("repo.release.draft"), 10)
+	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.12", translation.NewLocale("en-US").TrString("repo.release.draft"), 10)
 
 	// Check that user4 does not see draft and still see 10 latest releases
 	session2 := loginUser(t, "user4")
-	checkLatestReleaseAndCount(t, session2, "/user2/repo1", "v0.0.11", translation.NewLocale("en-US").Tr("repo.release.stable"), 10)
+	checkLatestReleaseAndCount(t, session2, "/user2/repo1", "v0.0.11", translation.NewLocale("en-US").TrString("repo.release.stable"), 10)
 }
 
 func TestViewReleaseListNoLogin(t *testing.T) {
@@ -234,7 +234,7 @@ func TestViewTagsList(t *testing.T) {
 
 	tagNames := make([]string, 0, 5)
 	tags.Each(func(i int, s *goquery.Selection) {
-		tagNames = append(tagNames, s.Find(".tag a.gt-df.gt-ac").Text())
+		tagNames = append(tagNames, s.Find(".tag a.tw-flex.tw-items-center").Text())
 	})
 
 	assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames)
diff --git a/tests/integration/rename_branch_test.go b/tests/integration/rename_branch_test.go
index 703fc243a4..13f6cf204b 100644
--- a/tests/integration/rename_branch_test.go
+++ b/tests/integration/rename_branch_test.go
@@ -5,17 +5,23 @@ package integration
 
 import (
 	"net/http"
+	"net/url"
 	"testing"
 
 	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	gitea_context "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
 )
 
 func TestRenameBranch(t *testing.T) {
+	onGiteaRun(t, testRenameBranch)
+}
+
+func testRenameBranch(t *testing.T, u *url.URL) {
 	defer tests.PrepareTestEnv(t)()
 
 	unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: 1, Name: "master"})
@@ -26,20 +32,19 @@ func TestRenameBranch(t *testing.T) {
 	resp := session.MakeRequest(t, req, http.StatusOK)
 	htmlDoc := NewHTMLParser(t, resp.Body)
 
-	postData := map[string]string{
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{
 		"_csrf": htmlDoc.GetCSRF(),
 		"from":  "master",
 		"to":    "main",
-	}
-	req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", postData)
+	})
 	session.MakeRequest(t, req, http.StatusSeeOther)
 
 	// check new branch link
-	req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/main/README.md", postData)
+	req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/main/README.md", nil)
 	session.MakeRequest(t, req, http.StatusOK)
 
 	// check old branch link
-	req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/master/README.md", postData)
+	req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/master/README.md", nil)
 	resp = session.MakeRequest(t, req, http.StatusSeeOther)
 	location := resp.Header().Get("Location")
 	assert.Equal(t, "/user2/repo1/src/branch/main/README.md", location)
@@ -47,4 +52,69 @@ func TestRenameBranch(t *testing.T) {
 	// check db
 	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	assert.Equal(t, "main", repo1.DefaultBranch)
+
+	// create branch1
+	csrf := GetCSRF(t, session, "/user2/repo1/src/branch/main")
+
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/main", map[string]string{
+		"_csrf":           csrf,
+		"new_branch_name": "branch1",
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+
+	branch1 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"})
+	assert.Equal(t, "branch1", branch1.Name)
+
+	// create branch2
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/main", map[string]string{
+		"_csrf":           csrf,
+		"new_branch_name": "branch2",
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+
+	branch2 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"})
+	assert.Equal(t, "branch2", branch2.Name)
+
+	// rename branch2 to branch1
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{
+		"_csrf": htmlDoc.GetCSRF(),
+		"from":  "branch2",
+		"to":    "branch1",
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+	flashCookie := session.GetCookie(gitea_context.CookieNameFlash)
+	assert.NotNil(t, flashCookie)
+	assert.Contains(t, flashCookie.Value, "error")
+
+	branch2 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"})
+	assert.Equal(t, "branch2", branch2.Name)
+	branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"})
+	assert.Equal(t, "branch1", branch1.Name)
+
+	// delete branch1
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/delete", map[string]string{
+		"_csrf": htmlDoc.GetCSRF(),
+		"name":  "branch1",
+	})
+	session.MakeRequest(t, req, http.StatusOK)
+	branch2 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"})
+	assert.Equal(t, "branch2", branch2.Name)
+	branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"})
+	assert.True(t, branch1.IsDeleted) // virtual deletion
+
+	// rename branch2 to branch1 again
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{
+		"_csrf": htmlDoc.GetCSRF(),
+		"from":  "branch2",
+		"to":    "branch1",
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+
+	flashCookie = session.GetCookie(gitea_context.CookieNameFlash)
+	assert.NotNil(t, flashCookie)
+	assert.Contains(t, flashCookie.Value, "success")
+
+	unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"})
+	branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"})
+	assert.Equal(t, "branch1", branch1.Name)
 }
diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go
index 91674ddc82..baa8da4b75 100644
--- a/tests/integration/repo_branch_test.go
+++ b/tests/integration/repo_branch_test.go
@@ -52,37 +52,37 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
 			OldRefSubURL:   "branch/master",
 			NewBranch:      "feature/test1",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test1"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature/test1"),
 		},
 		{
 			OldRefSubURL:   "branch/master",
 			NewBranch:      "",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("form.NewBranchName") + translation.NewLocale("en-US").Tr("form.require_error"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("form.NewBranchName") + translation.NewLocale("en-US").TrString("form.require_error"),
 		},
 		{
 			OldRefSubURL:   "branch/master",
 			NewBranch:      "feature=test1",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature=test1"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature=test1"),
 		},
 		{
 			OldRefSubURL:   "branch/master",
 			NewBranch:      strings.Repeat("b", 101),
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("form.NewBranchName") + translation.NewLocale("en-US").Tr("form.max_size_error", "100"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("form.NewBranchName") + translation.NewLocale("en-US").TrString("form.max_size_error", "100"),
 		},
 		{
 			OldRefSubURL:   "branch/master",
 			NewBranch:      "master",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.branch_already_exists", "master"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.branch_already_exists", "master"),
 		},
 		{
 			OldRefSubURL:   "branch/master",
 			NewBranch:      "master/test",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.branch_name_conflict", "master/test", "master"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.branch_name_conflict", "master/test", "master"),
 		},
 		{
 			OldRefSubURL:   "commit/acd1d892867872cb47f3993468605b8aa59aa2e0",
@@ -93,21 +93,21 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
 			OldRefSubURL:   "commit/65f1bf27bc3bf70f64657658635e66094edbcb4d",
 			NewBranch:      "feature/test3",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test3"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature/test3"),
 		},
 		{
 			OldRefSubURL:   "branch/master",
 			NewBranch:      "v1.0.0",
 			CreateRelease:  "v1.0.0",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.tag_collision", "v1.0.0"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.tag_collision", "v1.0.0"),
 		},
 		{
 			OldRefSubURL:   "tag/v1.0.0",
 			NewBranch:      "feature/test4",
 			CreateRelease:  "v1.0.1",
 			ExpectedStatus: http.StatusSeeOther,
-			FlashMessage:   translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test4"),
+			FlashMessage:   translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature/test4"),
 		},
 	}
 	for _, test := range tests {
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go
index 594fba6796..ca5d61ecc2 100644
--- a/tests/integration/repo_fork_test.go
+++ b/tests/integration/repo_fork_test.go
@@ -29,14 +29,14 @@ func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkO
 
 	// Step2: click the fork button
 	htmlDoc := NewHTMLParser(t, resp.Body)
-	link, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/fork/\"]").Attr("href")
+	link, exists := htmlDoc.doc.Find(`a.ui.button[href*="/fork"]`).Attr("href")
 	assert.True(t, exists, "The template has changed")
 	req = NewRequest(t, "GET", link)
 	resp = session.MakeRequest(t, req, http.StatusOK)
 
 	// Step3: fill the form of the forking
 	htmlDoc = NewHTMLParser(t, resp.Body)
-	link, exists = htmlDoc.doc.Find("form.ui.form[action^=\"/repo/fork/\"]").Attr("action")
+	link, exists = htmlDoc.doc.Find(`form.ui.form[action*="/fork"]`).Attr("action")
 	assert.True(t, exists, "The template has changed")
 	_, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", forkOwner.ID)).Attr("data-value")
 	assert.True(t, exists, fmt.Sprintf("Fork owner '%s' is not present in select box", forkOwnerName))
@@ -70,6 +70,6 @@ func TestRepoForkToOrg(t *testing.T) {
 	req := NewRequest(t, "GET", "/user2/repo1")
 	resp := session.MakeRequest(t, req, http.StatusOK)
 	htmlDoc := NewHTMLParser(t, resp.Body)
-	_, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/fork/\"]").Attr("href")
+	_, exists := htmlDoc.doc.Find(`a.ui.button[href*="/fork"]`).Attr("href")
 	assert.False(t, exists, "Forking should not be allowed anymore")
 }
diff --git a/tests/integration/repo_search_test.go b/tests/integration/repo_search_test.go
index cf199e98c2..56cc45d901 100644
--- a/tests/integration/repo_search_test.go
+++ b/tests/integration/repo_search_test.go
@@ -32,7 +32,7 @@ func TestSearchRepo(t *testing.T) {
 	repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1")
 	assert.NoError(t, err)
 
-	executeIndexer(t, repo, code_indexer.UpdateRepoIndexer)
+	code_indexer.UpdateRepoIndexer(repo)
 
 	testSearch(t, "/user2/repo1/search?q=Description&page=1", []string{"README.md"})
 
@@ -42,12 +42,14 @@ func TestSearchRepo(t *testing.T) {
 	repo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "glob")
 	assert.NoError(t, err)
 
-	executeIndexer(t, repo, code_indexer.UpdateRepoIndexer)
+	code_indexer.UpdateRepoIndexer(repo)
 
 	testSearch(t, "/user2/glob/search?q=loren&page=1", []string{"a.txt"})
-	testSearch(t, "/user2/glob/search?q=file3&page=1", []string{"x/b.txt"})
-	testSearch(t, "/user2/glob/search?q=file4&page=1", []string{})
-	testSearch(t, "/user2/glob/search?q=file5&page=1", []string{})
+	testSearch(t, "/user2/glob/search?q=loren&page=1&t=match", []string{"a.txt"})
+	testSearch(t, "/user2/glob/search?q=file3&page=1", []string{"x/b.txt", "a.txt"})
+	testSearch(t, "/user2/glob/search?q=file3&page=1&t=match", []string{"x/b.txt", "a.txt"})
+	testSearch(t, "/user2/glob/search?q=file4&page=1&t=match", []string{"x/b.txt", "a.txt"})
+	testSearch(t, "/user2/glob/search?q=file5&page=1&t=match", []string{"x/b.txt", "a.txt"})
 }
 
 func testSearch(t *testing.T, url string, expected []string) {
@@ -57,7 +59,3 @@ func testSearch(t *testing.T, url string, expected []string) {
 	filenames := resultFilenames(t, NewHTMLParser(t, resp.Body))
 	assert.EqualValues(t, expected, filenames)
 }
-
-func executeIndexer(t *testing.T, repo *repo_model.Repository, op func(*repo_model.Repository)) {
-	op(repo)
-}
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go
index f141b6dcb1..06c55b1e8a 100644
--- a/tests/integration/repo_test.go
+++ b/tests/integration/repo_test.go
@@ -77,7 +77,7 @@ func testViewRepo(t *testing.T) {
 		})
 
 		// convert "2017-06-14 21:54:21 +0800" to "Wed, 14 Jun 2017 13:54:21 UTC"
-		htmlTimeString, _ := s.Find("relative-time.time-since").Attr("datetime")
+		htmlTimeString, _ := s.Find("relative-time").Attr("datetime")
 		htmlTime, _ := time.Parse(time.RFC3339, htmlTimeString)
 		f.commitTime = htmlTime.In(time.Local).Format(time.RFC1123)
 		items = append(items, f)
diff --git a/tests/integration/repo_topic_test.go b/tests/integration/repo_topic_test.go
index 58fee8418f..f198397007 100644
--- a/tests/integration/repo_topic_test.go
+++ b/tests/integration/repo_topic_test.go
@@ -21,20 +21,42 @@ func TestTopicSearch(t *testing.T) {
 		TopicNames []*api.TopicResponse `json:"topics"`
 	}
 
+	// search all topics
+	res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+	DecodeJSON(t, res, &topics)
+	assert.Len(t, topics.TopicNames, 6)
+	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+	// pagination search topics
+	topics.TopicNames = nil
 	query := url.Values{"page": []string{"1"}, "limit": []string{"4"}}
 
 	searchURL.RawQuery = query.Encode()
-	res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
 	DecodeJSON(t, res, &topics)
 	assert.Len(t, topics.TopicNames, 4)
 	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
 
+	// second page
+	topics.TopicNames = nil
+	query = url.Values{"page": []string{"2"}, "limit": []string{"4"}}
+
+	searchURL.RawQuery = query.Encode()
+	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+	DecodeJSON(t, res, &topics)
+	assert.Len(t, topics.TopicNames, 2)
+	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+	// add keyword search
+	topics.TopicNames = nil
+	query = url.Values{"page": []string{"1"}, "limit": []string{"4"}}
 	query.Add("q", "topic")
 	searchURL.RawQuery = query.Encode()
 	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
 	DecodeJSON(t, res, &topics)
 	assert.Len(t, topics.TopicNames, 2)
 
+	topics.TopicNames = nil
 	query.Set("q", "database")
 	searchURL.RawQuery = query.Encode()
 	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go
new file mode 100644
index 0000000000..ef44a9e2d0
--- /dev/null
+++ b/tests/integration/repo_webhook_test.go
@@ -0,0 +1,41 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"net/http"
+	"strings"
+	"testing"
+
+	"code.gitea.io/gitea/tests"
+
+	"github.com/PuerkitoBio/goquery"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewWebHookLink(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	session := loginUser(t, "user2")
+
+	baseurl := "/user2/repo1/settings/hooks"
+	tests := []string{
+		// webhook list page
+		baseurl,
+		// new webhook page
+		baseurl + "/gitea/new",
+		// edit webhook page
+		baseurl + "/1",
+	}
+
+	for _, url := range tests {
+		resp := session.MakeRequest(t, NewRequest(t, "GET", url), http.StatusOK)
+		htmlDoc := NewHTMLParser(t, resp.Body)
+		menus := htmlDoc.doc.Find(".ui.top.attached.header .ui.dropdown .menu a")
+		menus.Each(func(i int, menu *goquery.Selection) {
+			url, exist := menu.Attr("href")
+			assert.True(t, exist)
+			assert.True(t, strings.HasPrefix(url, baseurl))
+		})
+	}
+}
diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go
index 19fbd1754c..49abeb83fb 100644
--- a/tests/integration/repofiles_change_test.go
+++ b/tests/integration/repofiles_change_test.go
@@ -12,11 +12,11 @@ import (
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/contexttest"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/contexttest"
 	files_service "code.gitea.io/gitea/services/repository/files"
 
 	"github.com/stretchr/testify/assert"
diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go
index 2584b88f65..77e19bba96 100644
--- a/tests/integration/signin_test.go
+++ b/tests/integration/signin_test.go
@@ -49,10 +49,10 @@ func TestSignin(t *testing.T) {
 		password string
 		message  string
 	}{
-		{username: "wrongUsername", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")},
-		{username: "wrongUsername", password: "password", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")},
-		{username: "user15", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")},
-		{username: "user1@example.com", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")},
+		{username: "wrongUsername", password: "wrongPassword", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")},
+		{username: "wrongUsername", password: "password", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")},
+		{username: "user15", password: "wrongPassword", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")},
+		{username: "user1@example.com", password: "wrongPassword", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")},
 	}
 
 	for _, s := range samples {
diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go
index f983f98ad8..e9a05201ee 100644
--- a/tests/integration/signup_test.go
+++ b/tests/integration/signup_test.go
@@ -12,6 +12,7 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/tests"
 
@@ -58,7 +59,7 @@ func TestSignupAsRestricted(t *testing.T) {
 	assert.True(t, user2.IsRestricted)
 }
 
-func TestSignupEmail(t *testing.T) {
+func TestSignupEmailValidation(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	setting.Service.EnableCaptcha = false
@@ -68,9 +69,9 @@ func TestSignupEmail(t *testing.T) {
 		wantStatus int
 		wantMsg    string
 	}{
-		{"exampleUser@example.com\r\n", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")},
-		{"exampleUser@example.com\r", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")},
-		{"exampleUser@example.com\n", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")},
+		{"exampleUser@example.com\r\n", http.StatusOK, translation.NewLocale("en-US").TrString("form.email_invalid")},
+		{"exampleUser@example.com\r", http.StatusOK, translation.NewLocale("en-US").TrString("form.email_invalid")},
+		{"exampleUser@example.com\n", http.StatusOK, translation.NewLocale("en-US").TrString("form.email_invalid")},
 		{"exampleUser@example.com", http.StatusSeeOther, ""},
 	}
 
@@ -91,3 +92,62 @@ func TestSignupEmail(t *testing.T) {
 		}
 	}
 }
+
+func TestSignupEmailActive(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)()
+
+	// try to sign up and send the activation email
+	req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
+		"user_name": "test-user-1",
+		"email":     "email-1@example.com",
+		"password":  "password1",
+		"retype":    "password1",
+	})
+	resp := MakeRequest(t, req, http.StatusOK)
+	assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to <b>email-1@example.com</b>.`)
+
+	// access "user/activate" means trying to re-send the activation email
+	session := loginUserWithPassword(t, "test-user-1", "password1")
+	resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate"), http.StatusOK)
+	assert.Contains(t, resp.Body.String(), "You have already requested an activation email recently")
+
+	// access anywhere else will see a "Activate Your Account" prompt, and there is a chance to change email
+	resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/issues"), http.StatusOK)
+	assert.Contains(t, resp.Body.String(), `<input id="change-email" name="change_email" `)
+
+	// post to "user/activate" with a new email
+	session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user/activate", map[string]string{"change_email": "email-changed@example.com"}), http.StatusSeeOther)
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
+	assert.Equal(t, "email-changed@example.com", user.Email)
+	email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "email-changed@example.com"})
+	assert.False(t, email.IsActivated)
+	assert.True(t, email.IsPrimary)
+
+	// access "user/activate" with a valid activation code, then get the "verify password" page
+	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
+	activationCode := user.GenerateEmailActivateCode(user.Email)
+	resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate?code="+activationCode), http.StatusOK)
+	assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)
+
+	// try to use a wrong password, it should fail
+	req = NewRequestWithValues(t, "POST", "/user/activate", map[string]string{
+		"code":     activationCode,
+		"password": "password-wrong",
+	})
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	assert.Contains(t, resp.Body.String(), `Your password does not match`)
+	assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)
+	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
+	assert.False(t, user.IsActive)
+
+	// then use a correct password, the user should be activated
+	req = NewRequestWithValues(t, "POST", "/user/activate", map[string]string{
+		"code":     activationCode,
+		"password": "password1",
+	})
+	resp = session.MakeRequest(t, req, http.StatusSeeOther)
+	assert.Equal(t, "/", test.RedirectURL(resp))
+	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
+	assert.True(t, user.IsActive)
+}
diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go
index d8e4c64e85..c4544f37aa 100644
--- a/tests/integration/user_test.go
+++ b/tests/integration/user_test.go
@@ -85,7 +85,7 @@ func TestRenameInvalidUsername(t *testing.T) {
 		htmlDoc := NewHTMLParser(t, resp.Body)
 		assert.Contains(t,
 			htmlDoc.doc.Find(".ui.negative.message").Text(),
-			translation.NewLocale("en-US").Tr("form.username_error"),
+			translation.NewLocale("en-US").TrString("form.username_error"),
 		)
 
 		unittest.AssertNotExistsBean(t, &user_model.User{Name: invalidUsername})
@@ -147,7 +147,7 @@ func TestRenameReservedUsername(t *testing.T) {
 		htmlDoc := NewHTMLParser(t, resp.Body)
 		assert.Contains(t,
 			htmlDoc.doc.Find(".ui.negative.message").Text(),
-			translation.NewLocale("en-US").Tr("user.form.name_reserved", reservedUsername),
+			translation.NewLocale("en-US").TrString("user.form.name_reserved", reservedUsername),
 		)
 
 		unittest.AssertNotExistsBean(t, &user_model.User{Name: reservedUsername})
@@ -243,6 +243,8 @@ func testExportUserGPGKeys(t *testing.T, user, expected string) {
 }
 
 func TestGetUserRss(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
 	user34 := "the_34-user.with.all.allowedChars"
 	req := NewRequestf(t, "GET", "/%s.rss", user34)
 	resp := MakeRequest(t, req, http.StatusOK)
@@ -253,6 +255,13 @@ func TestGetUserRss(t *testing.T) {
 		description, _ := rssDoc.ChildrenFiltered("description").Html()
 		assert.EqualValues(t, "&lt;p dir=&#34;auto&#34;&gt;some &lt;a href=&#34;https://commonmark.org/&#34; rel=&#34;nofollow&#34;&gt;commonmark&lt;/a&gt;!&lt;/p&gt;\n", description)
 	}
+
+	req = NewRequestf(t, "GET", "/non-existent-user.rss")
+	MakeRequest(t, req, http.StatusNotFound)
+
+	session := loginUser(t, "user2")
+	req = NewRequestf(t, "GET", "/non-existent-user.rss")
+	session.MakeRequest(t, req, http.StatusNotFound)
 }
 
 func TestListStopWatches(t *testing.T) {
diff --git a/build/generate-images.js b/tools/generate-images.js
similarity index 75%
rename from build/generate-images.js
rename to tools/generate-images.js
index a3a0f8d8f3..0bd3af29e4 100755
--- a/build/generate-images.js
+++ b/tools/generate-images.js
@@ -1,20 +1,13 @@
 #!/usr/bin/env node
-import imageminZopfli from 'imagemin-zopfli';
+import imageminZopfli from 'imagemin-zopfli'; // eslint-disable-line i/no-unresolved
+import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node'; // eslint-disable-line i/no-unresolved
 import {optimize} from 'svgo';
-import {fabric} from 'fabric';
 import {readFile, writeFile} from 'node:fs/promises';
+import {argv, exit} from 'node:process';
 
-function exit(err) {
+function doExit(err) {
   if (err) console.error(err);
-  process.exit(err ? 1 : 0);
-}
-
-function loadSvg(svg) {
-  return new Promise((resolve) => {
-    fabric.loadSVGFromString(svg, (objects, options) => {
-      resolve({objects, options});
-    });
-  });
+  exit(err ? 1 : 0);
 }
 
 async function generate(svg, path, {size, bg}) {
@@ -27,7 +20,7 @@ async function generate(svg, path, {size, bg}) {
         'removeDimensions',
         {
           name: 'addAttributesToSVGElement',
-          params: {attributes: [{width: size}, {height: size}]}
+          params: {attributes: [{width: size}, {height: size}]},
         },
       ],
     });
@@ -35,14 +28,14 @@ async function generate(svg, path, {size, bg}) {
     return;
   }
 
-  const {objects, options} = await loadSvg(svg);
-  const canvas = new fabric.Canvas();
+  const {objects, options} = await loadSVGFromString(svg);
+  const canvas = new Canvas();
   canvas.setDimensions({width: size, height: size});
   const ctx = canvas.getContext('2d');
   ctx.scale(options.width ? (size / options.width) : 1, options.height ? (size / options.height) : 1);
 
   if (bg) {
-    canvas.add(new fabric.Rect({
+    canvas.add(new Rect({
       left: 0,
       top: 0,
       height: size * (1 / (size / options.height)),
@@ -51,7 +44,7 @@ async function generate(svg, path, {size, bg}) {
     }));
   }
 
-  canvas.add(fabric.util.groupSVGElements(objects, options));
+  canvas.add(util.groupSVGElements(objects, options));
   canvas.renderAll();
 
   let png = Buffer.from([]);
@@ -64,7 +57,7 @@ async function generate(svg, path, {size, bg}) {
 }
 
 async function main() {
-  const gitea = process.argv.slice(2).includes('gitea');
+  const gitea = argv.slice(2).includes('gitea');
   const logoSvg = await readFile(new URL('../assets/logo.svg', import.meta.url), 'utf8');
   const faviconSvg = await readFile(new URL('../assets/favicon.svg', import.meta.url), 'utf8');
 
@@ -79,4 +72,8 @@ async function main() {
   ]);
 }
 
-main().then(exit).catch(exit);
+try {
+  doExit(await main());
+} catch (err) {
+  doExit(err);
+}
diff --git a/build/generate-svg.js b/tools/generate-svg.js
similarity index 92%
rename from build/generate-svg.js
rename to tools/generate-svg.js
index b845da9367..f744162099 100755
--- a/build/generate-svg.js
+++ b/tools/generate-svg.js
@@ -4,15 +4,16 @@ import {optimize} from 'svgo';
 import {parse} from 'node:path';
 import {readFile, writeFile, mkdir} from 'node:fs/promises';
 import {fileURLToPath} from 'node:url';
+import {exit} from 'node:process';
 
 const glob = (pattern) => fastGlob.sync(pattern, {
   cwd: fileURLToPath(new URL('..', import.meta.url)),
   absolute: true,
 });
 
-function exit(err) {
+function doExit(err) {
   if (err) console.error(err);
-  process.exit(err ? 1 : 0);
+  exit(err ? 1 : 0);
 }
 
 async function processFile(file, {prefix, fullName} = {}) {
@@ -38,8 +39,8 @@ async function processFile(file, {prefix, fullName} = {}) {
           attributes: [
             {'xmlns': 'http://www.w3.org/2000/svg'},
             {'width': '16'}, {'height': '16'}, {'aria-hidden': 'true'},
-          ]
-        }
+          ],
+        },
       },
     ],
   });
@@ -63,4 +64,8 @@ async function main() {
   ]);
 }
 
-main().then(exit).catch(exit);
+try {
+  doExit(await main());
+} catch (err) {
+  doExit(err);
+}
diff --git a/tools/lint-templates-svg.js b/tools/lint-templates-svg.js
new file mode 100755
index 0000000000..72f756400d
--- /dev/null
+++ b/tools/lint-templates-svg.js
@@ -0,0 +1,26 @@
+#!/usr/bin/env node
+import {readdirSync, readFileSync} from 'node:fs';
+import {parse, relative} from 'node:path';
+import {fileURLToPath} from 'node:url';
+import {exit} from 'node:process';
+import fastGlob from 'fast-glob';
+
+const knownSvgs = new Set();
+for (const file of readdirSync(new URL('../public/assets/img/svg', import.meta.url))) {
+  knownSvgs.add(parse(file).name);
+}
+
+const rootPath = fileURLToPath(new URL('..', import.meta.url));
+let hadErrors = false;
+
+for (const file of fastGlob.sync(fileURLToPath(new URL('../templates/**/*.tmpl', import.meta.url)))) {
+  const content = readFileSync(file, 'utf8');
+  for (const [_, name] of content.matchAll(/svg ["'`]([^"'`]+)["'`]/g)) {
+    if (!knownSvgs.has(name)) {
+      console.info(`SVG "${name}" not found, used in ${relative(rootPath, file)}`);
+      hadErrors = true;
+    }
+  }
+}
+
+exit(hadErrors ? 1 : 0);
diff --git a/build/watch.sh b/tools/watch.sh
similarity index 100%
rename from build/watch.sh
rename to tools/watch.sh
diff --git a/updates.config.js b/updates.config.js
new file mode 100644
index 0000000000..11908dea8e
--- /dev/null
+++ b/updates.config.js
@@ -0,0 +1,6 @@
+export default {
+  exclude: [
+    '@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled
+    'eslint-plugin-array-func', // need to migrate to eslint flat config first
+  ],
+};
diff --git a/vitest.config.js b/vitest.config.js
index 9a6cb4e560..ea0fafeee8 100644
--- a/vitest.config.js
+++ b/vitest.config.js
@@ -5,8 +5,8 @@ import {stringPlugin} from 'vite-string-plugin';
 export default defineConfig({
   test: {
     include: ['web_src/**/*.test.js'],
-    setupFiles: ['./web_src/js/test/setup.js'],
-    environment: 'jsdom',
+    setupFiles: ['web_src/js/vitest.setup.js'],
+    environment: 'happy-dom',
     testTimeout: 20000,
     open: false,
     allowOnly: true,
diff --git a/web_src/css/actions.css b/web_src/css/actions.css
index e353a013a7..1d5bea2395 100644
--- a/web_src/css/actions.css
+++ b/web_src/css/actions.css
@@ -14,10 +14,6 @@
   color: var(--color-red-light);
 }
 
-.runner-container .runner-basic-info .gt-dib {
-  margin-right: 1em;
-}
-
 .runner-container .runner-new-text {
   color: var(--color-white);
 }
@@ -48,7 +44,7 @@
 }
 
 .run-list-item-right {
-  flex: 0 0 15%;
+  flex: 0 0 min(20%, 130px);
   display: flex;
   flex-direction: column;
   gap: 3px;
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 198e87c0e2..c6a22a5dc4 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -21,13 +21,54 @@
   --border-radius-circle: 50%;
   --opacity-disabled: 0.55;
   --height-loading: 16rem;
+  --min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */
   --tab-size: 4;
+  --checkbox-size: 15px; /* height and width of checkbox and radio inputs */
+  --page-spacing: 16px; /* space between page elements */
+  --page-margin-x: 32px; /* minimum space on left and right side of page */
+}
+
+@media (min-width: 768px) and (max-width: 1200px) {
+  :root {
+    --page-margin-x: 16px;
+  }
+}
+
+@media (max-width: 767.98px) {
+  :root {
+    --page-margin-x: 8px;
+  }
 }
 
 :root * {
   --fonts-regular: var(--fonts-override, var(--fonts-proportional)), "Noto Sans", "Liberation Sans", sans-serif, var(--fonts-emoji);
 }
 
+*, ::before, ::after {
+  /* these are needed for tailwind borders to work because we do not load tailwind's base
+     https://github.com/tailwindlabs/tailwindcss/blob/master/src/css/preflight.css */
+  border-width: 0;
+  border-style: solid;
+  border-color: currentcolor;
+}
+
+html, body {
+  height: 100%;
+  font-size: 14px;
+}
+
+body {
+  line-height: 20px;
+  font-family: var(--fonts-regular);
+  color: var(--color-text);
+  background-color: var(--color-body);
+  tab-size: var(--tab-size);
+  display: flex;
+  flex-direction: column;
+  overflow-x: visible;
+  overflow-wrap: break-word;
+}
+
 textarea {
   font-family: var(--fonts-regular);
 }
@@ -36,10 +77,17 @@ pre,
 code,
 kbd,
 samp {
-  font-size: 0.9em; /* compensate for monospace fonts being usually slightly larger */
   font-family: var(--fonts-monospace);
 }
 
+pre,
+code,
+kbd,
+samp,
+.tw-font-mono {
+  font-size: 0.95em; /* compensate for monospace fonts being usually slightly larger */
+}
+
 b,
 strong,
 h1,
@@ -51,13 +99,65 @@ h6 {
   font-weight: var(--font-weight-semibold);
 }
 
-body {
-  color: var(--color-text);
-  background-color: var(--color-body);
-  tab-size: var(--tab-size);
-  display: flex;
-  flex-direction: column;
-  overflow-wrap: break-word;
+h1,
+h2,
+h3,
+h4,
+h5 {
+  line-height: 1.28571429;
+  margin: calc(2rem - 0.1428571428571429em) 0 1rem;
+  font-weight: var(--font-weight-medium);
+  padding: 0;
+}
+
+h1 {
+  min-height: 1rem;
+  font-size: 2rem;
+}
+
+h2 {
+  font-size: 1.71428571rem;
+}
+
+h3 {
+  font-size: 1.28571429rem;
+}
+
+h4 {
+  font-size: 1.07142857rem;
+}
+
+h5 {
+  font-size: 1rem;
+}
+
+h1:first-child,
+h2:first-child,
+h3:first-child,
+h4:first-child,
+h5:first-child {
+  margin-top: 0;
+}
+
+h1:last-child,
+h2:last-child,
+h3:last-child,
+h4:last-child,
+h5:last-child {
+  margin-bottom: 0;
+}
+
+p {
+  margin: 0 0 1em;
+  line-height: 1.4285;
+}
+
+p:first-child {
+  margin-top: 0;
+}
+
+p:last-child {
+  margin-bottom: 0;
 }
 
 table {
@@ -96,33 +196,9 @@ progress::-moz-progress-bar {
 }
 
 * {
-  scrollbar-color: var(--color-primary) transparent;
   caret-color: var(--color-caret);
 }
 
-::-webkit-scrollbar {
-  width: 10px;
-  height: 10px;
-}
-
-::-webkit-scrollbar-thumb {
-  box-shadow: inset 0 0 0 6px var(--color-primary);
-  border: 2px solid transparent;
-  border-radius: var(--border-radius);
-}
-
-::-webkit-scrollbar-thumb:window-inactive {
-  box-shadow: inset 0 0 0 6px var(--color-primary);
-}
-
-::-webkit-scrollbar-thumb:hover {
-  box-shadow: inset 0 0 0 6px var(--color-primary-dark-2);
-}
-
-::-webkit-scrollbar-corner {
-  background: transparent;
-}
-
 ::file-selector-button {
   border: 1px solid var(--color-light-border);
   color: var(--color-text-light);
@@ -136,8 +212,8 @@ progress::-moz-progress-bar {
 }
 
 ::selection {
-  background: var(--color-primary-light-1) !important;
-  color: var(--color-white) !important;
+  background: var(--color-primary-light-1);
+  color: var(--color-white);
 }
 
 ::placeholder,
@@ -161,28 +237,41 @@ progress::-moz-progress-bar {
 a {
   color: var(--color-primary);
   cursor: pointer;
+  text-decoration-line: none;
   text-decoration-skip-ink: all;
 }
 
-/* muted link = only colored when hovered */
-/* silenced link = never colored */
+a:hover {
+  text-decoration-line: underline;
+}
+
+/* a = always colored, underlined on hover */
+/* a.muted = colored on hover, underlined on hover */
+/* a.suppressed = never colored, underlined on hover */
+/* a.silenced = never colored, never underlined */
 
 a.muted,
+a.suppressed,
 a.silenced,
 .muted-links a {
   color: inherit;
 }
 
 a:hover,
+a.suppressed:hover,
 a.muted:hover,
 a.muted:hover [class*="color-text"],
 .muted-links a:hover {
   color: var(--color-primary);
 }
 
-a.silenced:hover {
+a.silenced:hover,
+a.suppressed:hover {
   color: inherit;
-  text-decoration: none;
+}
+
+a.silenced:hover {
+  text-decoration-line: none;
 }
 
 a.label,
@@ -190,7 +279,7 @@ a.label,
 .ui .menu a,
 .ui.cards a.card,
 .issue-keyword a {
-  text-decoration: none !important;
+  text-decoration-line: none !important;
 }
 
 .ui.search > .results {
@@ -226,71 +315,19 @@ a.label,
 
 .inline-code-block {
   padding: 2px 4px;
-  border-radius: var(--border-radius-medium);
-  background-color: var(--color-markup-code-block);
+  border-radius: .24em;
+  background-color: var(--color-label-bg);
 }
 
-.ui.dividing.header {
-  border-bottom-color: var(--color-secondary);
-}
-
-.page-content {
-  margin-top: 15px;
-}
-
-.page-content .header-wrapper,
-.page-content .new-menu {
-  margin-top: -15px !important;
-  padding-top: 15px !important;
-}
-
-/* fix Fomantic's line-height cutting off "g" on Windows Chrome with Segoe UI */
-.ui.input > input {
-  line-height: var(--line-height-default);
-  text-align: start; /* Override fomantic's `text-align: left` to make RTL work via HTML `dir="auto"` */
-}
-
-/* fix Fomantic's line-height causing vertical scrollbars to appear */
-ul.ui.list li,
-ol.ui.list li,
-.ui.list > .item,
-.ui.list .list > .item {
-  line-height: var(--line-height-default);
-}
-
-.ui.input.focus > input,
-.ui.input > input:focus {
-  border-color: var(--color-primary);
-}
-
-.ui.action.input .ui.ui.button {
-  border-color: var(--color-input-border);
-  padding-top: 0; /* the ".action.input" is "flex + stretch", so let the buttons layout themselves */
-  padding-bottom: 0;
-}
-
-/* currently used for search bar dropdowns in repo search and explore code */
-.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection {
-  min-width: 10em;
-}
-
-.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus,:hover) {
-  border-right-color: transparent;
-}
-
-.ui.action.input:not([class*="left action"]) > input,
-.ui.action.input:not([class*="left action"]) > input:hover {
-  border-right: 1px solid transparent;
-}
-
-.ui.action.input:not([class*="left action"]) > input:focus {
-  border-right-color: var(--color-primary);
+.ui.menu {
+  display: flex;
 }
 
 .ui.menu,
 .ui.vertical.menu {
   background: var(--color-menu);
   border-color: var(--color-secondary);
+  box-shadow: none;
 }
 
 .ui.menu .item {
@@ -413,26 +450,18 @@ ol.ui.list li,
   color: var(--color-text-light-2);
 }
 
+/* extend fomantic style '.ui.dropdown > .text > img' to include svg.img */
+.ui.dropdown > .text > .img {
+  margin-left: 0;
+  float: none;
+  margin-right: 0.78571429rem;
+}
+
 .ui.dropdown > .text > .description,
 .ui.dropdown .menu > .item > .description {
   color: var(--color-text-light-2);
 }
 
-.ui.list .list > .item .header,
-.ui.list > .item .header {
-  color: var(--color-text-dark);
-}
-
-.ui.list .list > .item > .content,
-.ui.list > .item > .content {
-  color: var(--color-text);
-}
-
-.ui.list .list > .item .description,
-.ui.list > .item .description {
-  color: var(--color-text);
-}
-
 /* replace item margin on secondary menu items with gap and remove both the
    negative margins on the menu as well as margin on the items */
 .ui.secondary.menu {
@@ -485,6 +514,11 @@ ol.ui.list li,
   background: var(--color-active) !important;
 }
 
+.ui.form textarea:not([rows]) {
+  height: var(--min-height-textarea); /* override fomantic default 12em */
+  min-height: var(--min-height-textarea); /* override fomantic default 8em */
+}
+
 /* styles from removed fomantic transition module */
 .hidden.transition {
   visibility: hidden;
@@ -495,93 +529,6 @@ ol.ui.list li,
   visibility: visible !important;
 }
 
-.ui.message {
-  background: var(--color-box-body);
-  color: var(--color-text);
-  box-shadow: none !important;
-  border: 1px solid var(--color-secondary);
-}
-
-.ui.info.message .header,
-.ui.blue.message .header {
-  color: var(--color-blue);
-}
-
-.ui.info.message,
-.ui.attached.info.message,
-.ui.blue.message,
-.ui.attached.blue.message {
-  background: var(--color-info-bg);
-  color: var(--color-info-text);
-  border-color: var(--color-info-border);
-}
-
-.ui.success.message .header,
-.ui.positive.message .header,
-.ui.green.message .header {
-  color: var(--color-green);
-}
-
-.ui.success.message,
-.ui.attached.success.message,
-.ui.positive.message,
-.ui.attached.positive.message {
-  background: var(--color-success-bg);
-  color: var(--color-success-text);
-  border-color: var(--color-success-border);
-}
-
-.ui.error.message .header,
-.ui.negative.message .header,
-.ui.red.message .header {
-  color: var(--color-red);
-}
-
-.ui.error.message,
-.ui.attached.error.message,
-.ui.red.message,
-.ui.attached.red.message,
-.ui.negative.message,
-.ui.attached.negative.message {
-  background: var(--color-error-bg);
-  color: var(--color-error-text);
-  border-color: var(--color-error-border);
-}
-
-.ui.warning.message .header,
-.ui.yellow.message .header {
-  color: var(--color-yellow);
-}
-
-.ui.warning.message,
-.ui.attached.warning.message,
-.ui.yellow.message,
-.ui.attached.yellow.message {
-  background: var(--color-warning-bg);
-  color: var(--color-warning-text);
-  border-color: var(--color-warning-border);
-}
-
-.ui.error.header {
-  background: var(--color-error-bg) !important;
-  color: var(--color-error-text) !important;
-  border-color: var(--color-error-border) !important;
-}
-
-.ui.error.segment {
-  border-color: var(--color-error-border) !important;
-}
-
-.ui.warning.header {
-  background: var(--color-warning-bg) !important;
-  color: var(--color-warning-text) !important;
-  border-color: var(--color-warning-border) !important;
-}
-
-.ui.warning.segment {
-  border-color: var(--color-warning-border) !important;
-}
-
 .ui.selection.active.dropdown,
 .ui.selection.active.dropdown:hover,
 .ui.selection.active.dropdown .menu,
@@ -589,10 +536,6 @@ ol.ui.list li,
   border-color: var(--color-primary);
 }
 
-.ui.selection.dropdown .menu {
-  margin: 0 -1.25px;
-}
-
 .ui.pointing.dropdown > .menu:not(.hidden)::after {
   background: var(--color-menu);
   box-shadow: -1px -1px 0 0 var(--color-secondary);
@@ -629,86 +572,6 @@ ol.ui.list li,
   color: var(--color-primary);
 }
 
-.ui.attached.table {
-  border-color: var(--color-secondary);
-}
-
-.ui.table {
-  color: var(--color-text);
-  background: var(--color-box-body);
-  border-color: var(--color-secondary);
-  text-align: start; /* Override fomantic's `text-align: left` to make RTL work via HTML `dir="auto"` */
-}
-
-.ui.table th,
-.ui.table td {
-  transition: none;
-}
-
-.ui.table > tr > td,
-.ui.table > tbody > tr > td {
-  border-top-color: var(--color-secondary-alpha-50);
-}
-
-.ui.striped.table > tr:nth-child(2n),
-.ui.striped.table > tbody > tr:nth-child(2n),
-.ui.basic.striped.table > tbody > tr:nth-child(2n) {
-  background: var(--color-light);
-}
-
-.ui.ui.ui.ui.table tr.active,
-.ui.ui.table td.active {
-  color: var(--color-text);
-  background: var(--color-active);
-}
-
-.ui.ui.selectable.table > tbody > tr:hover,
-.ui.table tbody tr td.selectable:hover {
-  color: var(--color-text);
-  background-color: var(--color-secondary-alpha-40);
-}
-
-.ui.ui.ui.ui.table tr.grey:not(.marked),
-.ui.ui.table td.grey:not(.marked) {
-  background: var(--color-body);
-  color: var(--color-text);
-}
-
-.ui.table > thead > tr > th {
-  background: var(--color-box-header);
-  border-color: var(--color-secondary);
-  color: var(--color-text);
-}
-
-.ui.basic.table > tbody > tr {
-  border-color: var(--color-secondary);
-}
-
-.ui.table > tfoot > tr > th,
-.ui.table > tfoot > tr > td {
-  border-color: var(--color-secondary);
-  background: var(--color-box-body);
-  color: var(--color-text);
-}
-
-/* reduce table padding, needed especially for dense admin tables */
-.ui.table > thead > tr > th,
-.ui.table > tbody > tr > td,
-.ui.table > tr > td {
-  padding: 6px 5px;
-}
-/* use more horizontal padding on first and last items for visuals */
-.ui.table > thead > tr > th:first-of-type,
-.ui.table > tbody > tr > td:first-of-type,
-.ui.table > tr > td:first-of-type {
-  padding-left: 10px;
-}
-.ui.table > thead > tr > th:last-of-type,
-.ui.table > tbody > tr > td:last-of-type,
-.ui.table > tr > td:last-of-type {
-  padding-right: 10px;
-}
-
 img.ui.avatar,
 .ui.avatar img,
 .ui.avatar svg {
@@ -717,10 +580,6 @@ img.ui.avatar,
   aspect-ratio: 1;
 }
 
-.ui.divided.list > .item {
-  border-color: var(--color-secondary);
-}
-
 .ui.error.message .header,
 .ui.warning.message .header {
   color: inherit;
@@ -732,35 +591,19 @@ img.ui.avatar,
   padding-bottom: 80px;
 }
 
-/* overwrite semantic width of containers inside the main page content div (div with class "page-content") */
-.page-content .ui.ui.ui.container:not(.fluid) {
-  width: 1280px;
-  max-width: calc(100% - 64px);
-  margin-left: auto;
-  margin-right: auto;
+/* add margin below .secondary nav when it is the first child */
+.page-content > :first-child.secondary-nav {
+  margin-bottom: 14px;
 }
 
-.ui.container.fluid.padded {
-  padding: 0 32px;
+/* add margin to all pages when there is no .secondary.nav */
+.page-content > :first-child:not(.secondary-nav) {
+  margin-top: var(--page-spacing);
 }
-
-/* enable fluid page widths for medium size viewports */
-@media (min-width: 768px) and (max-width: 1200px) {
-  .page-content .ui.ui.ui.container:not(.fluid) {
-    max-width: calc(100% - 32px);
-  }
-  .ui.container.fluid.padded {
-    padding: 0 16px;
-  }
-}
-
-@media (max-width: 767.98px) {
-  .page-content .ui.ui.ui.container:not(.fluid) {
-    max-width: calc(100% - 16px);
-  }
-  .ui.container.fluid.padded {
-    padding: 0 8px;
-  }
+/* if .ui.grid is the first child the first grid-column has 'padding-top: 1rem' which we need
+   to compensate here */
+.page-content > :first-child.ui.grid {
+  margin-top: calc(var(--page-spacing) - 1rem);
 }
 
 .ui.pagination.menu .active.item {
@@ -768,16 +611,6 @@ img.ui.avatar,
   background: var(--color-active);
 }
 
-.ui.loading.segment::before,
-.ui.loading.form::before {
-  background: none;
-}
-
-.ui.loading.form > *,
-.ui.loading.segment > * {
-  opacity: 0.35;
-}
-
 .ui.form .fields.error .field textarea,
 .ui.form .fields.error .field select,
 .ui.form .fields.error .field input:not([type]),
@@ -851,20 +684,24 @@ img.ui.avatar,
   border-color: var(--color-error-border) !important;
 }
 
-/* A fix for text visibility issue in Chrome autofill in dark mode. */
-/* It's a problem from Formatic UI, and this rule overrides it. */
-.ui.form .field.field input:-webkit-autofill {
-  -webkit-text-fill-color: var(--color-black) !important;
+input:-webkit-autofill,
+input:-webkit-autofill:focus,
+input:-webkit-autofill:hover,
+input:-webkit-autofill:active,
+.ui.form .field.field input:-webkit-autofill,
+.ui.form .field.field input:-webkit-autofill:focus,
+.ui.form .field.field input:-webkit-autofill:hover,
+.ui.form .field.field input:-webkit-autofill:active {
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: var(--color-text);
+  box-shadow: 0 0 0 100px var(--color-primary-light-6) inset !important;
+  border-color: var(--color-primary-light-4) !important;
 }
 
 .ui.form .field.muted {
   opacity: var(--opacity-disabled);
 }
 
-.ui.loading.loading.input > i.icon svg {
-  visibility: hidden;
-}
-
 .text.primary {
   color: var(--color-primary) !important;
 }
@@ -949,20 +786,6 @@ img.ui.avatar,
   font-weight: var(--font-weight-normal);
 }
 
-.ui.floating.label {
-  z-index: 10;
-}
-
-.ui.transparent.label {
-  background-color: transparent;
-}
-
-.ui.menu,
-.ui.vertical.menu,
-.ui.segment {
-  box-shadow: none;
-}
-
 /* replace fomantic popover box shadows */
 .ui.dropdown .menu,
 .ui.upward.dropdown > .menu,
@@ -997,14 +820,6 @@ img.ui.avatar,
   width: 100%;
 }
 
-.ui.dropdown .menu > .item > .floating.label {
-  z-index: 11;
-}
-
-.ui.dropdown .menu .menu > .item > .floating.label {
-  z-index: 21;
-}
-
 .ui.dropdown .menu > .header {
   font-size: 0.8em;
 }
@@ -1056,23 +871,6 @@ img.ui.avatar,
   vertical-align: middle;
 }
 
-.ui .info.segment.top h3,
-.ui .info.segment.top h4 {
-  margin-top: 0;
-}
-
-.ui .info.segment.top h3:last-child {
-  margin-top: 4px;
-}
-
-.ui .info.segment.top > :last-child {
-  margin-bottom: 0;
-}
-
-.ui .normal.header {
-  font-weight: var(--font-weight-normal);
-}
-
 .ui .form .autofill-dummy {
   position: absolute;
   width: 1px;
@@ -1230,17 +1028,6 @@ img.ui.avatar,
   margin-right: 0;
 }
 
-.ui.icon.header svg {
-  width: 3em;
-  height: 3em;
-  float: none;
-  display: block;
-  line-height: var(--line-height-default);
-  padding: 0;
-  margin: 0 auto 0.5rem;
-  opacity: 1;
-}
-
 .ui.floating.dropdown .overflow.menu .scrolling.menu.items {
   border-radius: 0 !important;
   box-shadow: none !important;
@@ -1269,97 +1056,64 @@ img.ui.avatar,
 }
 
 .attention-icon {
-  vertical-align: text-top;
+  margin: 2px 6px 0 0;
 }
 
-.attention-note {
-  font-weight: unset;
-  color: var(--color-info-text);
+blockquote.attention-note {
+  border-left-color: var(--color-blue-dark-1);
+}
+strong.attention-note, svg.attention-note {
+  color: var(--color-blue-dark-1);
 }
 
-.attention-warning {
-  font-weight: unset;
+blockquote.attention-tip {
+  border-left-color: var(--color-success-text);
+}
+strong.attention-tip, svg.attention-tip {
+  color: var(--color-success-text);
+}
+
+blockquote.attention-important {
+  border-left-color: var(--color-violet-dark-1);
+}
+strong.attention-important, svg.attention-important {
+  color: var(--color-violet-dark-1);
+}
+
+blockquote.attention-warning {
+  border-left-color: var(--color-warning-text);
+}
+strong.attention-warning, svg.attention-warning {
   color: var(--color-warning-text);
 }
 
+blockquote.attention-caution {
+  border-left-color: var(--color-red-dark-1);
+}
+strong.attention-caution, svg.attention-caution {
+  color: var(--color-red-dark-1);
+}
+
 .center:not(.popup) {
   text-align: center;
 }
 
-@media (max-width: 767.98px) {
-  /* double selector so it wins over .gt-df etc */
-  .not-mobile.not-mobile {
-    display: none !important;
-  }
-}
-
-.ui.menu.new-menu {
-  margin-bottom: 15px;
-  background: var(--color-header-wrapper);
+overflow-menu {
   border-bottom: 1px solid var(--color-secondary) !important;
-  overflow: auto;
-}
-
-.ui.menu.new-menu .new-menu-inner {
   display: flex;
-  margin-left: auto;
-  margin-right: auto;
-  overflow-x: auto;
-  width: 100%;
-  mask-image: linear-gradient(to right, #000 0%, #000 calc(100% - 60px), transparent 100%);
-  -webkit-mask-image: linear-gradient(to right, #000 0%, #000 calc(100% - 60px), transparent 100%);
 }
 
-.ui.menu.new-menu .item {
+overflow-menu .overflow-menu-items {
+  display: flex;
+  flex: 1;
+}
+
+overflow-menu .overflow-menu-items .item {
   margin-bottom: 0 !important; /* reset fomantic's margin, because the active menu has special bottom border */
 }
 
-@media (max-width: 767.98px) {
-  .ui.menu.new-menu .item {
-    width: auto !important;
-  }
-}
-
-.ui.menu.new-menu .item:first-child {
-  margin-left: auto; /* "justify-content: center" doesn't work with "overflow: auto", so use margin: auto */
-}
-
-.ui.menu.new-menu .item:last-child {
-  padding-right: 30px !important;
-  margin-right: auto;
-}
-
-.ui.menu.new-menu::-webkit-scrollbar {
-  height: 6px;
-  display: none;
-}
-
-.ui.menu.new-menu::-webkit-scrollbar-track {
-  background: none !important;
-}
-
-.ui.menu.new-menu::-webkit-scrollbar-thumb {
-  box-shadow: none !important;
-}
-
-.ui.menu.new-menu:hover::-webkit-scrollbar {
-  display: block;
-}
-
-.repos-search {
-  padding-bottom: 0 !important;
-}
-
-.repos-filter {
-  margin-top: 0 !important;
-  border-bottom-width: 0 !important;
-  margin-bottom: 2px !important;
-  justify-content: space-evenly;
-}
-
-.ui.secondary.pointing.menu.repos-filter .item {
-  padding-left: 4.5px;
-  padding-right: 4.5px;
+overflow-menu .ui.label {
+  margin-left: 7px !important; /* save some space */
 }
 
 .activity-bar-graph {
@@ -1395,55 +1149,21 @@ img.ui.avatar,
   margin-top: 1px;
 }
 
-.ui.label {
-  padding: 0.3em 0.5em;
-  transition: none;
-  white-space: nowrap;
-}
-
-.ui.label,
 .ui.menu .item > .label {
   background: var(--color-label-bg);
   color: var(--color-label-text);
 }
 
-.ui.label > a {
-  opacity: .75; /* increase contrast over default fomantic .5 */
-}
-
-.ui.active.label {
-  background: var(--color-label-active-bg);
-  border-color: var(--color-label-active-bg);
-  color: var(--color-label-text);
-}
-
-.ui.labels a.label:hover,
-a.ui.label:hover {
-  background: var(--color-label-hover-bg);
-  border-color: var(--color-label-hover-bg);
-  color: var(--color-label-text);
-}
-
-.ui.labels a.active.label:hover,
-a.ui.active.label:hover {
-  background: var(--color-label-active-bg);
-  border-color: var(--color-label-active-bg);
-  color: var(--color-label-text);
-}
-
 .lines-blame-btn {
-  padding-left: 10px;
-  padding-right: 10px;
-  text-align: right !important;
-  background-color: var(--color-code-sidebar-bg);
-  width: 2%;
+  padding: 0 0 0 5px;
+  display: flex;
+  justify-content: center;
 }
 
 .lines-num {
-  padding-left: 10px;
-  padding-right: 10px;
+  padding: 0 8px;
   text-align: right !important;
-  color: var(--color-text-light-1);
+  color: var(--color-text-light-2);
   width: 1%;
   font-family: var(--fonts-monospace);
 }
@@ -1456,10 +1176,13 @@ a.ui.active.label:hover {
   content: attr(data-line-number);
   line-height: 20px !important;
   padding: 0 10px;
-  cursor: pointer;
   display: block;
 }
 
+.code-view .lines-num span::after {
+  cursor: pointer;
+}
+
 .lines-type-marker {
   vertical-align: top;
 }
@@ -1497,22 +1220,34 @@ a.ui.active.label:hover {
 }
 
 .lines-code {
-  background-color: var(--color-code-bg);
   padding-left: 5px;
 }
 
-.lines-code.active,
-.lines-code .active {
-  background: var(--color-active-line) !important;
+.file-view tr.active {
+  color: inherit !important;
+  background: inherit !important;
 }
 
-.blame .lines-num {
-  padding: 0 !important;
-  background-color: var(--color-code-sidebar-bg);
+.file-view tr.active .lines-num,
+.file-view tr.active .lines-code {
+  background: var(--color-highlight-bg) !important;
 }
 
-.blame .lines-code {
-  padding: 0 !important;
+.file-view tr.active:last-of-type .lines-code {
+  border-bottom-right-radius: var(--border-radius);
+}
+
+.file-view tr.active .lines-num {
+  position: relative;
+}
+
+.file-view tr.active .lines-num::before {
+  content: "";
+  position: absolute;
+  left: 0;
+  width: 2px;
+  height: 100%;
+  background: var(--color-highlight-fg);
 }
 
 .code-inner {
@@ -1520,27 +1255,25 @@ a.ui.active.label:hover {
   white-space: pre-wrap;
   word-break: break-all;
   overflow-wrap: anywhere;
+  line-height: inherit; /* needed for inline code preview in markup */
 }
 
 .blame .code-inner {
-  white-space: pre;
-  word-break: normal;
-  word-wrap: normal; /* not using overflow-wrap because safari does not treat is an an alias */
+  white-space: pre-wrap;
+  overflow-wrap: anywhere;
 }
 
 .lines-commit {
   vertical-align: top;
-  color: var(--color-grey);
+  color: var(--color-text-light-1);
   padding: 0 !important;
-  background: var(--color-code-sidebar-bg);
   width: 1%;
 }
 
 .lines-commit .blame-info {
-  width: 350px;
-  max-width: 350px;
+  width: min(26vw, 300px);
   display: block;
-  padding: 0 0 0 10px;
+  padding: 0 0 0 6px;
   line-height: 20px;
   box-sizing: content-box;
 }
@@ -1562,166 +1295,34 @@ a.ui.active.label:hover {
   flex-shrink: 0;
 }
 
-.lines-commit .ui.avatar {
-  height: 18px;
-  width: 18px;
-  display: block;
-  margin-top: 1px;
+.blame-avatar {
+  display: flex;
+  align-items: center;
+  margin-right: 4px;
 }
 
 .top-line-blame {
   border-top: 1px solid var(--color-secondary);
 }
 
+.code-view tr.top-line-blame:first-of-type {
+  border-top: none;
+}
+
 .lines-code .bottom-line,
 .lines-commit .bottom-line {
   border-bottom: 1px solid var(--color-secondary);
 }
 
+.code-view {
+  background: var(--color-code-bg);
+  border-radius: var(--border-radius);
+}
+
 .code-view table {
   width: 100%;
 }
 
-.ui.primary.label,
-.ui.primary.labels .label,
-.ui.ui.ui.primary.label {
-  background-color: var(--color-primary);
-  border-color: var(--color-primary-dark-2);
-}
-
-.ui.basic.labels .primary.label,
-.ui.ui.ui.basic.primary.label {
-  background: transparent;
-  border-color: var(--color-primary);
-  color: var(--color-primary);
-}
-
-.ui.basic.labels a.primary.label:hover,
-a.ui.ui.ui.basic.primary.label:hover {
-  background: var(--color-hover);
-  border-color: var(--color-primary-dark-1);
-  color: var(--color-primary-dark-1);
-}
-
-.ui.basic.labels .secondary.label,
-.ui.ui.ui.basic.secondary.label {
-  background: transparent;
-  border-color: var(--color-secondary);
-  color: var(--color-secondary);
-}
-
-.ui.basic.labels .orange.label,
-.ui.ui.ui.basic.orange.label {
-  background: transparent;
-  border-color: var(--color-orange);
-  color: var(--color-orange);
-}
-
-.ui.basic.labels .green.label,
-.ui.ui.ui.basic.green.label {
-  background: transparent;
-  border-color: var(--color-green);
-  color: var(--color-green);
-}
-
-.ui.basic.labels .olive.label,
-.ui.ui.ui.basic.olive.label {
-  background: transparent;
-  border-color: var(--color-olive);
-  color: var(--color-olive);
-}
-
-.ui.basic.labels .teal.label,
-.ui.ui.ui.basic.teal.label {
-  background: transparent;
-  border-color: var(--color-teal);
-  color: var(--color-teal);
-}
-
-.ui.basic.labels .blue.label,
-.ui.ui.ui.basic.blue.label {
-  background: transparent;
-  border-color: var(--color-blue);
-  color: var(--color-blue);
-}
-
-.ui.basic.labels .violet.label,
-.ui.ui.ui.basic.violet.label {
-  background: transparent;
-  border-color: var(--color-violet);
-  color: var(--color-violet);
-}
-
-.ui.basic.labels .purple.label,
-.ui.ui.ui.basic.purple.label {
-  background: transparent;
-  border-color: var(--color-purple);
-  color: var(--color-purple);
-}
-
-.ui.basic.labels .pink.label,
-.ui.ui.ui.basic.pink.label {
-  background: transparent;
-  border-color: var(--color-pink);
-  color: var(--color-pink);
-}
-
-.ui.basic.labels .red.label,
-.ui.ui.ui.basic.red.label {
-  background: transparent;
-  border-color: var(--color-red);
-  color: var(--color-red);
-}
-
-.ui.basic.labels .brown.label,
-.ui.ui.ui.basic.brown.label {
-  background: transparent;
-  border-color: var(--color-brown);
-  color: var(--color-brown);
-}
-
-.ui.basic.labels .yellow.label,
-.ui.ui.ui.basic.yellow.label {
-  background: transparent;
-  border-color: var(--color-yellow);
-  color: var(--color-yellow);
-}
-
-.ui.basic.labels .grey.label,
-.ui.ui.ui.basic.grey.label {
-  background: transparent;
-  border-color: var(--color-grey);
-  color: var(--color-grey);
-}
-
-.ui.basic.labels .black.label,
-.ui.ui.ui.basic.black.label {
-  background: transparent;
-  border-color: var(--color-black);
-  color: var(--color-black);
-}
-
-.ui.basic.labels .label,
-.ui.basic.label,
-.ui.secondary.labels .ui.basic.label {
-  background: var(--color-button);
-  border-color: var(--color-light-border);
-  color: var(--color-text-light);
-}
-
-.ui.basic.labels a.label:hover,
-a.ui.basic.label:hover {
-  color: var(--color-text);
-  border-color: var(--color-light-border);
-  background: var(--color-hover);
-}
-
-.ui.label > img {
-  width: auto !important;
-  vertical-align: middle;
-  height: 2.1666em !important;
-}
-
 .migrate .svg.gitea-git {
   color: var(--color-git);
 }
@@ -1733,56 +1334,6 @@ a.ui.basic.label:hover {
   width: 14px;
 }
 
-.ui.label > .color-icon {
-  margin-left: 0;
-}
-
-.ui.segment,
-.ui.segments,
-.ui.attached.segment {
-  background: var(--color-box-body);
-  color: var(--color-text);
-  border-color: var(--color-secondary);
-}
-
-.ui.segments > .segment {
-  border-color: var(--color-secondary);
-}
-
-.ui.secondary.segment {
-  background: var(--color-secondary-bg);
-  color: var(--color-text-light);
-}
-
-.ui.attached.header {
-  position: relative;
-  background: var(--color-box-header);
-  border-color: var(--color-secondary);
-}
-
-/* fix misaligned right buttons on box headers */
-.ui.attached.header > .ui.right {
-  position: absolute;
-  right: 0.78571429rem;
-  top: 0;
-  bottom: 0;
-  display: flex;
-  align-items: center;
-  gap: 0.25em;
-}
-
-/* the default ".ui.attached.header > .ui.right" is only able to contain "tiny" buttons, other buttons are too large */
-.ui.attached.header > .ui.right .ui.tiny.button {
-  padding: 6px 10px;
-  font-weight: var(--font-weight-normal);
-}
-
-/* if a .top.attached.header is followed by a .segment, add some margin */
-.ui.segments + .ui.top.attached.header,
-.ui.attached.segment + .ui.top.attached.header {
-  margin-top: 1rem;
-}
-
 .rss-icon {
   display: inline-flex;
   color: var(--color-text-light-1);
@@ -1833,35 +1384,15 @@ table th[data-sortt-desc] .svg {
   vertical-align: -0.15em;
 }
 
-/* for the jquery.minicolors plugin */
-.minicolors-panel {
-  background: var(--color-secondary-dark-1) !important;
-}
-
-/* https://github.com/go-gitea/gitea/pull/11486 */
-.ui.sub.header {
-  text-transform: none;
-}
-
 .ui.tabular.menu {
   border-color: var(--color-secondary);
 }
 
-.ui.tabular.menu .item {
-  padding: 11px 12px;
-  color: var(--color-text-light-2);
-}
-
-.ui.tabular.menu .item:hover {
-  color: var(--color-text);
-}
-
 .ui.tabular.menu .active.item,
 .ui.tabular.menu .active.item:hover {
   background: var(--color-body);
   border-color: var(--color-secondary);
   color: var(--color-text);
-  margin-top: 1px; /* offset fomantic's margin-bottom: -1px */
 }
 
 .ui.segment .ui.tabular.menu .active.item,
@@ -1873,31 +1404,34 @@ table th[data-sortt-desc] .svg {
   border-color: var(--color-secondary);
 }
 
+.ui.tabular.menu .item,
 .ui.secondary.pointing.menu .item {
+  padding: 11.55px 12px !important; /* match .dashboard-navbar in height */
   color: var(--color-text-light-2);
 }
 
+.ui.tabular.menu .item:hover,
+.ui.secondary.pointing.menu a.item:hover {
+  color: var(--color-text);
+}
+
 .ui.secondary.pointing.menu .active.item,
 .ui.secondary.pointing.menu .active.item:hover,
-.ui.secondary.pointing.menu .dropdown.item:hover,
-.ui.secondary.pointing.menu a.item:hover {
+.ui.secondary.pointing.menu .dropdown.item:hover {
   color: var(--color-text-dark);
 }
 
-.ui.header {
-  color: var(--color-text);
+.ui.tabular.menu .active.item,
+.ui.secondary.pointing.menu .active.item,
+.resize-for-semibold::before {
+  font-weight: var(--font-weight-semibold);
 }
 
-.ui.header .ui.label {
-  margin-left: 0.25rem;
-}
-
-.ui.header > .ui.label.compact {
-  margin-top: inherit;
-}
-
-.ui.header .sub.header {
-  color: var(--color-text-light-1);
+.resize-for-semibold::before {
+  content: attr(data-text);
+  visibility: hidden;
+  display: block;
+  height: 0;
 }
 
 .flash-error details code,
@@ -1992,8 +1526,6 @@ table th[data-sortt-desc] .svg {
 .btn,
 .ui.ui.button,
 .ui.ui.dropdown,
-.ui.ui.label,
-.flex-items-inline > .item,
 .flex-text-inline {
   display: inline-flex;
   align-items: center;
@@ -2009,24 +1541,21 @@ table th[data-sortt-desc] .svg {
   vertical-align: middle;
 }
 
-.ui.ui.circular.label {
-  justify-content: center;
-}
-
 .ui.ui.labeled.button {
   gap: 0;
   align-items: stretch;
 }
 
-.ui.ui.icon.input .icon {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
 .flex-items-block > .item,
 .flex-text-block {
   display: flex;
   align-items: center;
   gap: .25rem;
 }
+
+/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content */
+.ui.dropdown .menu.flex-items-menu > .item {
+  display: flex !important;
+  align-items: center;
+  gap: .5rem;
+}
diff --git a/web_src/css/chroma/base.css b/web_src/css/chroma/base.css
index 26d128775f..bce13332f8 100644
--- a/web_src/css/chroma/base.css
+++ b/web_src/css/chroma/base.css
@@ -1,7 +1,3 @@
-.chroma {
-  background-color: var(--color-code-bg);
-}
-
 /* LineTableTD */
 .chroma .lntd {
   vertical-align: top;
diff --git a/web_src/css/dashboard.css b/web_src/css/dashboard.css
index 51ddd45e31..d61e0c1cf2 100644
--- a/web_src/css/dashboard.css
+++ b/web_src/css/dashboard.css
@@ -28,23 +28,6 @@
   width: 75%;
 }
 
-.dashboard.feeds .filter.menu .item .floating.label,
-.dashboard.issues .filter.menu .item .floating.label {
-  top: 7px;
-  left: 90%;
-  width: 15%;
-}
-
-@media (max-width: 767.98px) {
-  .dashboard.feeds .filter.menu .item .floating.label,
-  .dashboard.issues .filter.menu .item .floating.label {
-    top: 10px;
-    left: auto;
-    width: auto;
-    right: 13px;
-  }
-}
-
 /* Sort */
 .dashboard.feeds .filter.menu .jump.item,
 .dashboard.issues .filter.menu .jump.item {
@@ -77,15 +60,14 @@
   margin: 0 1px; /* Accommodate for Semantic's 1px hacks on .attached elements */
 }
 
-.dashboard .dashboard-navbar {
-  padding-left: 0.5rem;
-  padding-right: 0.5rem;
+.dashboard .secondary-nav {
+  padding: 1px 12px; /* match .overflow-menu-items in height */
 }
 
-.dashboard .dashboard-navbar .org-visibility .label {
+.dashboard .secondary-nav .org-visibility .label {
   margin-left: 5px;
 }
 
-.dashboard .dashboard-navbar .ui.dropdown {
+.dashboard .secondary-nav .ui.dropdown {
   max-width: 100%;
 }
diff --git a/web_src/css/editor/combomarkdowneditor.css b/web_src/css/editor/combomarkdowneditor.css
index 12ec1866a5..8a2f4ea416 100644
--- a/web_src/css/editor/combomarkdowneditor.css
+++ b/web_src/css/editor/combomarkdowneditor.css
@@ -37,13 +37,12 @@
 .combo-markdown-editor textarea.markdown-text-editor {
   display: block;
   width: 100%;
-  min-height: 200px;
-  max-height: calc(100vh - 200px);
+  max-height: calc(100vh - var(--min-height-textarea));
   resize: vertical;
 }
 
 .combo-markdown-editor .CodeMirror-scroll {
-  max-height: calc(100vh - 200px);
+  max-height: calc(100vh - var(--min-height-textarea));
 }
 
 /* use the same styles as markup/content.css */
diff --git a/web_src/css/explore.css b/web_src/css/explore.css
index 08858337c0..5cdee823c0 100644
--- a/web_src/css/explore.css
+++ b/web_src/css/explore.css
@@ -1,10 +1,8 @@
-.explore .navbar {
-  margin-bottom: 15px !important;
-  background-color: var(--color-header-wrapper) !important;
+.explore .secondary-nav {
   border-width: 1px !important;
 }
 
-.explore .navbar .svg {
+.explore .secondary-nav .svg {
   width: 16px;
   text-align: center;
   margin-right: 5px;
diff --git a/web_src/css/features/colorpicker.css b/web_src/css/features/colorpicker.css
new file mode 100644
index 0000000000..b7436783df
--- /dev/null
+++ b/web_src/css/features/colorpicker.css
@@ -0,0 +1,47 @@
+.js-color-picker-input {
+  display: flex;
+  position: relative;
+}
+
+.js-color-picker-input input {
+  padding-top: 8px !important;
+  padding-bottom: 8px !important;
+  padding-left: 32px !important;
+}
+
+.js-color-picker-input .preview-square {
+  position: absolute;
+  aspect-ratio: 1;
+  height: 16px;
+  left: 10px;
+  top: 50%;
+  transform: translateY(-50%);
+  border-radius: 2px;
+  background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
+  background-position: 0 0, 4px 4px;
+  background-size: 8px 8px;
+}
+
+.js-color-picker-input .preview-square::after {
+  content: "";
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  border-radius: inherit;
+  background-color: currentcolor;
+}
+
+hex-color-picker {
+  width: 180px;
+  height: 120px;
+}
+
+hex-color-picker::part(hue-pointer),
+hex-color-picker::part(saturation-pointer) {
+  width: 22px;
+  height: 22px;
+}
+
+hex-color-picker::part(hue) {
+  flex-basis: 16px;
+}
diff --git a/web_src/css/features/gitgraph.css b/web_src/css/features/gitgraph.css
index 795e1f2d61..6a04c44e51 100644
--- a/web_src/css/features/gitgraph.css
+++ b/web_src/css/features/gitgraph.css
@@ -4,12 +4,6 @@
   min-height: 350px;
 }
 
-#git-graph-container > .ui.segment.loading {
-  border: 0;
-  z-index: 1;
-  min-height: 246px;
-}
-
 #git-graph-container h2 {
   display: flex;
   justify-content: space-between;
diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css
index f85430a2a8..e23c146748 100644
--- a/web_src/css/features/projects.css
+++ b/web_src/css/features/projects.css
@@ -19,6 +19,11 @@
   overflow: visible;
   display: flex;
   flex-direction: column;
+  cursor: default;
+}
+
+.project-column .issue-card {
+  color: var(--color-text);
 }
 
 .project-column-header {
@@ -27,25 +32,15 @@
   justify-content: space-between;
 }
 
-.project-column-header.dark-label {
-  color: var(--color-project-board-dark-label) !important;
-}
-
-.project-column-header.dark-label .project-column-title {
-  color: var(--color-project-board-dark-label) !important;
-}
-
-.project-column-header.light-label {
-  color: var(--color-project-board-light-label) !important;
-}
-
-.project-column-header.light-label .project-column-title {
-  color: var(--color-project-board-light-label) !important;
-}
-
 .project-column-title {
   background: none !important;
   line-height: 1.25 !important;
+  cursor: inherit;
+}
+
+.project-column-title,
+.project-column-issue-count {
+  color: inherit !important;
 }
 
 .project-column > .cards {
@@ -62,6 +57,8 @@
 
 .project-column > .divider {
   margin: 5px 0;
+  border-color: currentcolor;
+  opacity: .5;
 }
 
 .project-column:first-child {
@@ -92,6 +89,7 @@
 }
 
 .card-ghost {
+  border-color: var(--color-secondary-dark-4) !important;
   border-style: dashed !important;
   background: none !important;
 }
@@ -99,26 +97,3 @@
 .card-ghost * {
   opacity: 0;
 }
-
-.color-field .minicolors.minicolors-theme-default {
-  display: block;
-}
-
-.color-field .minicolors.minicolors-theme-default .minicolors-input {
-  height: 38px;
-  padding-left: 2rem;
-}
-
-.color-field .minicolors.minicolors-theme-default .minicolors-swatch {
-  top: 10px;
-}
-
-.edit-project-column-modal .color.picker.column,
-.new-project-column-modal .color.picker.column {
-  display: flex;
-}
-
-.edit-project-column-modal .color.picker.column .minicolors,
-.new-project-column-modal .color.picker.column .minicolors {
-  flex: 1;
-}
diff --git a/web_src/css/form.css b/web_src/css/form.css
index e4efa34948..a8f73b6b66 100644
--- a/web_src/css/form.css
+++ b/web_src/css/form.css
@@ -32,15 +32,17 @@ textarea,
 .ui.form input[type="text"],
 .ui.form input[type="time"],
 .ui.form input[type="url"],
-.ui.selection.dropdown,
-.ui.checkbox label::before,
-.ui.checkbox input:checked ~ label::before,
-.ui.checkbox input:not([type="radio"]):indeterminate ~ label::before {
+.ui.selection.dropdown {
   background: var(--color-input-background);
   border-color: var(--color-input-border);
   color: var(--color-input-text);
 }
 
+/* fix fomantic small dropdown having inconsistent padding with input */
+.ui.small.selection.dropdown {
+  padding: .67857143em 3.2em .67857143em 1em;
+}
+
 input:hover,
 textarea:hover,
 .ui.input input:hover,
@@ -58,12 +60,7 @@ textarea:hover,
 .ui.form input[type="text"]:hover,
 .ui.form input[type="time"]:hover,
 .ui.form input[type="url"]:hover,
-.ui.selection.dropdown:hover,
-.ui.checkbox label:hover::before,
-.ui.checkbox label:active::before,
-.ui.radio.checkbox label::after,
-.ui.radio.checkbox input:focus ~ label::before,
-.ui.radio.checkbox input:checked ~ label::before {
+.ui.selection.dropdown:hover {
   background: var(--color-input-background);
   border-color: var(--color-input-border-hover);
   color: var(--color-input-text);
@@ -86,11 +83,7 @@ textarea:focus,
 .ui.form input[type="text"]:focus,
 .ui.form input[type="time"]:focus,
 .ui.form input[type="url"]:focus,
-.ui.selection.dropdown:focus,
-.ui.checkbox input:focus ~ label::before,
-.ui.checkbox input:not([type="radio"]):indeterminate:focus ~ label::before,
-.ui.checkbox input:checked:focus ~ label::before,
-.ui.radio.checkbox input:focus:checked ~ label::before {
+.ui.selection.dropdown:focus {
   background: var(--color-input-background);
   border-color: var(--color-primary);
   color: var(--color-input-text);
@@ -101,58 +94,21 @@ textarea:focus,
 .ui.form .inline.fields .field > label,
 .ui.form .inline.fields .field > p,
 .ui.form .inline.field > label,
-.ui.form .inline.field > p,
-.ui.checkbox label,
-.ui.checkbox + label,
-.ui.checkbox label:hover,
-.ui.checkbox + label:hover,
-.ui.checkbox input:focus ~ label,
-.ui.checkbox input:active ~ label {
+.ui.form .inline.field > p {
   color: var(--color-text);
 }
 
 .ui.form .required.fields:not(.grouped) > .field > label::after,
 .ui.form .required.fields.grouped > label::after,
 .ui.form .required.field > label::after,
-.ui.form .required.fields:not(.grouped) > .field > .checkbox::after,
-.ui.form .required.field > .checkbox::after,
 .ui.form label.required::after {
   color: var(--color-red);
 }
 
-.ui.input,
-.ui.checkbox input:focus ~ label::after,
-.ui.checkbox input:checked ~ label::after,
-.ui.checkbox label:active::after,
-.ui.checkbox input:not([type="radio"]):indeterminate ~ label::after,
-.ui.checkbox input:not([type="radio"]):indeterminate:focus ~ label::after,
-.ui.checkbox input:checked:focus ~ label::after,
-.ui.disabled.checkbox label,
-.ui.checkbox input[disabled] ~ label {
+.ui.input {
   color: var(--color-input-text);
 }
 
-.ui.radio.checkbox input:focus ~ label::after,
-.ui.radio.checkbox input:checked ~ label::after,
-.ui.radio.checkbox input:focus:checked ~ label::after {
-  background: var(--color-input-text);
-}
-
-.ui.toggle.checkbox label::before {
-  background: var(--color-input-toggle-background);
-}
-
-.ui.toggle.checkbox label,
-.ui.toggle.checkbox input:checked ~ label,
-.ui.toggle.checkbox input:focus:checked ~ label {
-  color: var(--color-text) !important;
-}
-
-.ui.toggle.checkbox input:checked ~ label::before,
-.ui.toggle.checkbox input:focus:checked ~ label::before {
-  background: var(--color-primary) !important;
-}
-
 /* match <select> padding to <input> */
 .ui.form select {
   padding: 0.67857143em 1em;
@@ -239,11 +195,8 @@ textarea:focus,
   }
 }
 
-.user.activate form,
 .user.forgot.password form,
 .user.reset.password form,
-.user.link-account form,
-.user.signin form,
 .user.signup form {
   margin: auto;
   width: 700px !important;
@@ -275,12 +228,7 @@ textarea:focus,
   .user.signup form .header {
     padding-left: 280px !important;
   }
-  .user.activate form .inline.field > label,
-  .user.forgot.password form .inline.field > label,
-  .user.reset.password form .inline.field > label,
-  .user.link-account form .inline.field > label,
-  .user.signin form .inline.field > label,
-  .user.signup form .inline.field > label {
+  .user.activate form .inline.field > label {
     text-align: right;
     width: 250px !important;
     word-wrap: break-word;
@@ -301,21 +249,6 @@ textarea:focus,
   .user.signup form .optional .title {
     margin-left: 250px !important;
   }
-  .user.activate form .inline.field > input,
-  .user.forgot.password form .inline.field > input,
-  .user.reset.password form .inline.field > input,
-  .user.link-account form .inline.field > input,
-  .user.signin form .inline.field > input,
-  .user.signup form .inline.field > input,
-  .user.activate form .inline.field > textarea,
-  .user.forgot.password form .inline.field > textarea,
-  .user.reset.password form .inline.field > textarea,
-  .user.link-account form .inline.field > textarea,
-  .user.signin form .inline.field > textarea,
-  .user.signup form .inline.field > textarea,
-  .oauth-login-link {
-    width: 50%;
-  }
 }
 
 @media (max-width: 767.98px) {
@@ -362,14 +295,7 @@ textarea:focus,
   .user.reset.password form .inline.field > label,
   .user.link-account form .inline.field > label,
   .user.signin form .inline.field > label,
-  .user.signup form .inline.field > label,
-  .user.activate form input,
-  .user.forgot.password form input,
-  .user.reset.password form input,
-  .user.link-account form input,
-  .user.signin form input,
-  .user.signup form input,
-  .oauth-login-link {
+  .user.signup form .inline.field > label {
     width: 100% !important;
   }
 }
@@ -487,9 +413,9 @@ textarea:focus,
   .repository.new.repo form label,
   .repository.new.migrate form label,
   .repository.new.fork form label,
-  .repository.new.repo form input,
-  .repository.new.migrate form input,
-  .repository.new.fork form input,
+  .repository.new.repo form .inline.field > input,
+  .repository.new.migrate form .inline.field > input,
+  .repository.new.fork form .inline.field > input,
   .repository.new.fork form .field a,
   .repository.new.repo form .selection.dropdown,
   .repository.new.migrate form .selection.dropdown,
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index da94ebb486..118c058b19 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -2,21 +2,6 @@
 Gitea's tailwind-style CSS helper classes have `gt-` prefix.
 Gitea's private styles use `g-` prefix.
 */
-.gt-df { display: flex !important; }
-.gt-dib { display: inline-block !important; }
-.gt-ac { align-items: center !important; }
-.gt-jc { justify-content: center !important; }
-.gt-je { justify-content: flex-end !important; }
-.gt-sb { justify-content: space-between !important; }
-.gt-fc { flex-direction: column !important; }
-.gt-f1 { flex: 1 !important; }
-.gt-fw { flex-wrap: wrap !important; }
-.gt-vm { vertical-align: middle !important; }
-
-.gt-mono {
-  font-family: var(--fonts-monospace) !important;
-  font-size: .95em !important; /* compensate for monospace fonts being usually slightly larger */
-}
 
 .gt-word-break {
   word-wrap: break-word !important;
@@ -46,82 +31,6 @@ Gitea's private styles use `g-` prefix.
   text-overflow: ellipsis;
 }
 
-.gt-max-width-12rem { max-width: 12rem !important; }
-.gt-max-width-24rem { max-width: 24rem !important; }
-
-/* below class names match Tailwind CSS */
-.gt-break-all { word-break: break-all !important; }
-.gt-content-center { align-content: center !important; }
-.gt-cursor-default { cursor: default !important; }
-.gt-cursor-pointer { cursor: pointer !important; }
-.gt-cursor-grab { cursor: grab !important; }
-.gt-invisible { visibility: hidden !important; }
-.gt-items-start { align-items: flex-start !important; }
-.gt-pointer-events-none { pointer-events: none !important; }
-.gt-relative { position: relative !important; }
-.gt-whitespace-nowrap { white-space: nowrap !important; }
-.gt-whitespace-pre { white-space: pre !important; }
-.gt-whitespace-pre-wrap { white-space: pre-wrap !important; }
-.gt-object-contain { object-fit: contain !important; }
-.gt-self-center { align-self: center !important; }
-.gt-self-start { align-self: flex-start !important; }
-.gt-self-end { align-self: flex-end !important; }
-.gt-no-underline { text-decoration-line: none !important; }
-.gt-normal-case { text-transform: none !important; }
-.gt-italic { font-style: italic !important; }
-.gt-overflow-x-auto { overflow-x: auto !important; }
-.gt-overflow-x-scroll { overflow-x: scroll !important; }
-.gt-overflow-y-hidden { overflow-y: hidden !important; }
-
-.gt-h-screen { height: 100vh !important; }
-.gt-h-full { height: 100% !important; }
-.gt-w-auto { width: auto !important; }
-.gt-w-screen { width: 100vw !important; }
-.gt-w-full { width: 100% !important; }
-
-.gt-float-left { float: left !important; }
-.gt-float-right { float: right !important; }
-.gt-clear-both { clear: both !important; }
-
-.gt-text-center { text-align: center !important; }
-.gt-text-left { text-align: left !important; }
-.gt-text-right { text-align: right !important; }
-
-.gt-font-light { font-weight: var(--font-weight-light) !important; }
-.gt-font-normal { font-weight: var(--font-weight-normal) !important; }
-.gt-font-medium { font-weight: var(--font-weight-medium) !important; }
-.gt-font-semibold { font-weight: var(--font-weight-semibold) !important; }
-.gt-font-bold { font-weight: var(--font-weight-bold) !important; }
-
-.gt-rounded { border-radius: var(--border-radius) !important; }
-.gt-rounded-top { border-radius: var(--border-radius) var(--border-radius) 0 0 !important; }
-.gt-rounded-bottom { border-radius: 0 0 var(--border-radius) var(--border-radius) !important; }
-.gt-rounded-left { border-radius: var(--border-radius) 0 0 var(--border-radius) !important; }
-.gt-rounded-right { border-radius: 0 var(--border-radius) var(--border-radius) 0 !important; }
-
-.gt-border-secondary { border: 1px solid var(--color-secondary) !important; }
-.gt-border-secondary-top { border-top: 1px solid var(--color-secondary) !important; }
-.gt-border-secondary-bottom { border-bottom: 1px solid var(--color-secondary) !important; }
-.gt-border-secondary-left { border-left: 1px solid var(--color-secondary) !important; }
-.gt-border-secondary-right { border-right: 1px solid var(--color-secondary) !important; }
-
-.gt-bg-red { background: var(--color-red) !important; }
-.gt-bg-orange { background: var(--color-orange) !important; }
-.gt-bg-yellow { background: var(--color-yellow) !important; }
-.gt-bg-olive { background: var(--color-olive) !important; }
-.gt-bg-green { background: var(--color-green) !important; }
-.gt-bg-teal { background: var(--color-teal) !important; }
-.gt-bg-blue { background: var(--color-blue) !important; }
-.gt-bg-violet { background: var(--color-violet) !important; }
-.gt-bg-purple { background: var(--color-purple) !important; }
-.gt-bg-pink { background: var(--color-pink) !important; }
-.gt-bg-brown { background: var(--color-brown) !important; }
-.gt-bg-grey { background: var(--color-grey) !important; }
-.gt-bg-gold { background: var(--color-gold) !important; }
-.gt-bg-transparent { background: transparent !important; }
-
-.gt-text-white { color: var(--color-white) !important; }
-
 .interact-fg { color: inherit !important; }
 .interact-fg:hover { color: var(--color-primary) !important; }
 .interact-fg:active { color: var(--color-primary-active) !important; }
@@ -130,152 +39,44 @@ Gitea's private styles use `g-` prefix.
 .interact-bg:hover { background: var(--color-hover) !important; }
 .interact-bg:active { background: var(--color-active) !important; }
 
-.gt-m-0 { margin: 0 !important; }
-.gt-m-1 { margin: .125rem !important; }
-.gt-m-2 { margin: .25rem !important; }
-.gt-m-3 { margin: .5rem !important; }
-.gt-m-4 { margin: 1rem !important; }
-.gt-m-5 { margin: 2rem !important; }
-
-.gt-ml-0 { margin-left: 0 !important; }
-.gt-ml-1 { margin-left: .125rem !important; }
-.gt-ml-2 { margin-left: .25rem !important; }
-.gt-ml-3 { margin-left: .5rem !important; }
-.gt-ml-4 { margin-left: 1rem !important; }
-.gt-ml-5 { margin-left: 2rem !important; }
-
-.gt-mr-0 { margin-right: 0 !important; }
-.gt-mr-1 { margin-right: .125rem !important; }
-.gt-mr-2 { margin-right: .25rem !important; }
-.gt-mr-3 { margin-right: .5rem !important; }
-.gt-mr-4 { margin-right: 1rem !important; }
-.gt-mr-5 { margin-right: 2rem !important; }
-
-.gt-mt-0 { margin-top: 0 !important; }
-.gt-mt-1 { margin-top: .125rem !important; }
-.gt-mt-2 { margin-top: .25rem !important; }
-.gt-mt-3 { margin-top: .5rem !important; }
-.gt-mt-4 { margin-top: 1rem !important; }
-.gt-mt-5 { margin-top: 2rem !important; }
-
-.gt-mb-0 { margin-bottom: 0 !important; }
-.gt-mb-1 { margin-bottom: .125rem !important; }
-.gt-mb-2 { margin-bottom: .25rem !important; }
-.gt-mb-3 { margin-bottom: .5rem !important; }
-.gt-mb-4 { margin-bottom: 1rem !important; }
-.gt-mb-5 { margin-bottom: 2rem !important; }
-
-.gt-mx-0 { margin-left: 0 !important; margin-right: 0 !important; }
-.gt-mx-1 { margin-left: .125rem !important; margin-right: .125rem !important; }
-.gt-mx-2 { margin-left: .25rem !important; margin-right: .25rem !important; }
-.gt-mx-3 { margin-left: .5rem !important; margin-right: .5rem !important; }
-.gt-mx-4 { margin-left: 1rem !important; margin-right: 1rem !important; }
-.gt-mx-5 { margin-left: 2rem !important; margin-right: 2rem !important; }
-
-.gt-my-0 { margin-top: 0 !important; margin-bottom: 0 !important; }
-.gt-my-1 { margin-top: .125rem !important; margin-bottom: .125rem !important; }
-.gt-my-2 { margin-top: .25rem !important; margin-bottom: .25rem !important; }
-.gt-my-3 { margin-top: .5rem !important; margin-bottom: .5rem !important; }
-.gt-my-4 { margin-top: 1rem !important; margin-bottom: 1rem !important; }
-.gt-my-5 { margin-top: 2rem !important; margin-bottom: 2rem !important; }
-
-.gt-m-auto  { margin: auto !important; }
-.gt-mx-auto { margin-left: auto !important; margin-right: auto !important; }
-.gt-my-auto { margin-top: auto !important; margin-bottom: auto !important; }
-.gt-mt-auto { margin-top: auto !important; }
-.gt-mr-auto { margin-right: auto !important; }
-.gt-mb-auto { margin-bottom: auto !important; }
-.gt-ml-auto { margin-left: auto !important; }
-
-.gt-p-0 { padding: 0 !important; }
-.gt-p-1 { padding: .125rem !important; }
-.gt-p-2 { padding: .25rem !important; }
-.gt-p-3 { padding: .5rem !important; }
-.gt-p-4 { padding: 1rem !important; }
-.gt-p-5 { padding: 2rem !important; }
-
-.gt-pl-0 { padding-left: 0 !important; }
-.gt-pl-1 { padding-left: .125rem !important; }
-.gt-pl-2 { padding-left: .25rem !important; }
-.gt-pl-3 { padding-left: .5rem !important; }
-.gt-pl-4 { padding-left: 1rem !important; }
-.gt-pl-5 { padding-left: 2rem !important; }
-
-.gt-pr-0 { padding-right: 0 !important; }
-.gt-pr-1 { padding-right: .125rem !important; }
-.gt-pr-2 { padding-right: .25rem !important; }
-.gt-pr-3 { padding-right: .5rem !important; }
-.gt-pr-4 { padding-right: 1rem !important; }
-.gt-pr-5 { padding-right: 2rem !important; }
-
-.gt-pt-0 { padding-top: 0 !important; }
-.gt-pt-1 { padding-top: .125rem !important; }
-.gt-pt-2 { padding-top: .25rem !important; }
-.gt-pt-3 { padding-top: .5rem !important; }
-.gt-pt-4 { padding-top: 1rem !important; }
-.gt-pt-5 { padding-top: 2rem !important; }
-
-.gt-pb-0 { padding-bottom: 0 !important; }
-.gt-pb-1 { padding-bottom: .125rem !important; }
-.gt-pb-2 { padding-bottom: .25rem !important; }
-.gt-pb-3 { padding-bottom: .5rem !important; }
-.gt-pb-4 { padding-bottom: 1rem !important; }
-.gt-pb-5 { padding-bottom: 2rem !important; }
-
-.gt-px-0 { padding-left: 0 !important; padding-right: 0 !important; }
-.gt-px-1 { padding-left: .125rem !important; padding-right: .125rem !important; }
-.gt-px-2 { padding-left: .25rem !important; padding-right: .25rem !important; }
-.gt-px-3 { padding-left: .5rem !important; padding-right: .5rem !important; }
-.gt-px-4 { padding-left: 1rem !important; padding-right: 1rem !important; }
-.gt-px-5 { padding-left: 2rem !important; padding-right: 2rem !important; }
-
-.gt-py-0 { padding-top: 0 !important; padding-bottom: 0 !important; }
-.gt-py-1 { padding-top: .125rem !important; padding-bottom: .125rem !important; }
-.gt-py-2 { padding-top: .25rem !important; padding-bottom: .25rem !important; }
-.gt-py-3 { padding-top: .5rem !important; padding-bottom: .5rem !important; }
-.gt-py-4 { padding-top: 1rem !important; padding-bottom: 1rem !important; }
-.gt-py-5 { padding-top: 2rem !important; padding-bottom: 2rem !important; }
-
-.gt-gap-0 { gap: 0 !important; }
-.gt-gap-1 { gap: .125rem !important; }
-.gt-gap-2 { gap: .25rem !important; }
-.gt-gap-3 { gap: .5rem !important; }
-.gt-gap-4 { gap: 1rem !important; }
-.gt-gap-5 { gap: 2rem !important; }
-
-.gt-gap-x-0 { column-gap: 0 !important; }
-.gt-gap-x-1 { column-gap: .125rem !important; }
-.gt-gap-x-2 { column-gap: .25rem !important; }
-.gt-gap-x-3 { column-gap: .5rem !important; }
-.gt-gap-x-4 { column-gap: 1rem !important; }
-.gt-gap-x-5 { column-gap: 2rem !important; }
-
-.gt-gap-y-0 { row-gap: 0 !important; }
-.gt-gap-y-1 { row-gap: .125rem !important; }
-.gt-gap-y-2 { row-gap: .25rem !important; }
-.gt-gap-y-3 { row-gap: .5rem !important; }
-.gt-gap-y-4 { row-gap: 1rem !important; }
-.gt-gap-y-5 { row-gap: 2rem !important; }
-
-.gt-shrink-0 { flex-shrink: 0 !important; }
-
-.gt-font-12 { font-size: 12px !important }
-.gt-font-13 { font-size: 13px !important }
-.gt-font-14 { font-size: 14px !important }
-.gt-font-15 { font-size: 15px !important }
-.gt-font-16 { font-size: 16px !important }
-.gt-font-17 { font-size: 17px !important }
-.gt-font-18 { font-size: 18px !important }
-
 /*
-gt-hidden must win all other "display: xxx !important" classes to get the chance to "hide" an element.
+tw-hidden must win all other "display: xxx !important" classes to get the chance to "hide" an element.
 do not use:
 * "[hidden]" attribute: it's too weak, can not be applied to an element with "display: flex"
 * ".hidden" class: it has been polluted by Fomantic UI in many cases
 * inline style="display: none": it's difficult to tweak
 * jQuery's show/hide/toggle: it can not show/hide elements with "display: xxx !important"
 only use:
-* this ".gt-hidden" class
+* this ".tw-hidden" class
 * showElem/hideElem/toggleElem functions in "utils/dom.js"
 */
-.gt-hidden.gt-hidden { display: none !important; }
+.tw-hidden.tw-hidden { display: none !important; }
+
+@media (max-width: 767.98px) {
+  /* double selector so it wins over .tw-flex (old .gt-df) etc */
+  .not-mobile.not-mobile {
+    display: none !important;
+  }
+}
+@media (min-width: 767.98px) {
+  .only-mobile.only-mobile {
+    display: none !important;
+  }
+}
+
+.tab-size-1 { tab-size: 1 !important; }
+.tab-size-2 { tab-size: 2 !important; }
+.tab-size-3 { tab-size: 3 !important; }
+.tab-size-4 { tab-size: 4 !important; }
+.tab-size-5 { tab-size: 5 !important; }
+.tab-size-6 { tab-size: 6 !important; }
+.tab-size-7 { tab-size: 7 !important; }
+.tab-size-8 { tab-size: 8 !important; }
+.tab-size-9 { tab-size: 9 !important; }
+.tab-size-10 { tab-size: 10 !important; }
+.tab-size-11 { tab-size: 11 !important; }
+.tab-size-12 { tab-size: 12 !important; }
+.tab-size-13 { tab-size: 13 !important; }
+.tab-size-14 { tab-size: 14 !important; }
+.tab-size-15 { tab-size: 15 !important; }
+.tab-size-16 { tab-size: 16 !important; }
diff --git a/web_src/css/index.css b/web_src/css/index.css
index f893531b78..ad59f32636 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -1,15 +1,28 @@
 @import "./modules/normalize.css";
 @import "./modules/animations.css";
+
+/* fomantic replacements */
 @import "./modules/button.css";
+@import "./modules/container.css";
+@import "./modules/divider.css";
+@import "./modules/header.css";
+@import "./modules/input.css";
+@import "./modules/label.css";
+@import "./modules/list.css";
+@import "./modules/segment.css";
+@import "./modules/grid.css";
+@import "./modules/message.css";
+@import "./modules/table.css";
+@import "./modules/card.css";
+@import "./modules/checkbox.css";
+@import "./modules/modal.css";
+
 @import "./modules/select.css";
 @import "./modules/tippy.css";
-@import "./modules/modal.css";
 @import "./modules/breadcrumb.css";
-@import "./modules/card.css";
 @import "./modules/comment.css";
 @import "./modules/navbar.css";
 @import "./modules/toast.css";
-@import "./modules/divider.css";
 @import "./modules/svg.css";
 @import "./modules/flexcontainer.css";
 
@@ -29,6 +42,7 @@
 
 @import "./markup/content.css";
 @import "./markup/codecopy.css";
+@import "./markup/codepreview.css";
 @import "./markup/asciicast.css";
 
 @import "./chroma/base.css";
@@ -59,4 +73,6 @@
 @import "./explore.css";
 @import "./review.css";
 @import "./actions.css";
+
+@tailwind utilities;
 @import "./helpers.css";
diff --git a/web_src/css/markup/codepreview.css b/web_src/css/markup/codepreview.css
new file mode 100644
index 0000000000..c9d19f5cc8
--- /dev/null
+++ b/web_src/css/markup/codepreview.css
@@ -0,0 +1,36 @@
+.markup .code-preview-container {
+  border: 1px solid var(--color-secondary);
+  border-radius: var(--border-radius);
+  margin: 0.25em 0;
+}
+
+.markup .code-preview-container .code-preview-header {
+  border-bottom: 1px solid var(--color-secondary);
+  padding: 0.5em;
+  font-size: 12px;
+}
+
+.markup .code-preview-container table {
+  width: 100%;
+  max-height: 240px; /* 12 lines at 20px per line */
+  overflow-y: auto;
+  margin: 0; /* override ".markup table {margin}" */
+}
+
+/* workaround to hide empty p before container - more details are in "html_codepreview.go" */
+.markup p:empty:has(+ .code-preview-container) {
+  display: none;
+}
+
+/* override the polluted styles from the content.css: ".markup table ..." */
+.markup .code-preview-container table tr {
+  border: 0 !important;
+}
+.markup .code-preview-container table th,
+.markup .code-preview-container table td {
+  border: 0 !important;
+  padding: 0 0 0 5px !important;
+}
+.markup .code-preview-container table tr:nth-child(2n) {
+  background: none !important;
+}
diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css
index caefa1605c..6ba4e40072 100644
--- a/web_src/css/markup/content.css
+++ b/web_src/css/markup/content.css
@@ -270,7 +270,7 @@
   margin-left: 0;
   padding: 0 15px;
   color: var(--color-text-light-2);
-  border-left: 4px solid var(--color-secondary);
+  border-left: 0.25em solid var(--color-secondary);
 }
 
 .markup blockquote > :first-child {
@@ -382,7 +382,7 @@
   text-align: center;
 }
 
-.markup span.align-center span img
+.markup span.align-center span img,
 .markup span.align-center span video {
   margin: 0 auto;
   text-align: center;
@@ -438,7 +438,7 @@
   margin: 0;
   font-size: 85%;
   white-space: break-spaces;
-  background-color: var(--color-markup-code-block);
+  background-color: var(--color-markup-code-inline);
   border-radius: var(--border-radius);
 }
 
@@ -508,7 +508,7 @@
   line-height: 10px;
   color: var(--color-text-light);
   vertical-align: middle;
-  background-color: var(--color-markup-code-block);
+  background-color: var(--color-markup-code-inline);
   border: 1px solid var(--color-secondary);
   border-radius: var(--border-radius);
   box-shadow: inset 0 -1px 0 var(--color-secondary);
diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css
index 87eb6a75cf..361618c449 100644
--- a/web_src/css/modules/animations.css
+++ b/web_src/css/modules/animations.css
@@ -6,13 +6,17 @@
 .is-loading {
   pointer-events: none !important;
   position: relative !important;
-  overflow: hidden !important;
 }
 
 .is-loading > * {
   opacity: 0.3;
 }
 
+.btn.is-loading > *,
+.button.is-loading > * {
+  opacity: 0;
+}
+
 .is-loading::after {
   content: "";
   position: absolute;
@@ -20,6 +24,7 @@
   left: 50%;
   top: 50%;
   height: min(4em, 66.6%);
+  width: fit-content; /* compat: safari - https://bugs.webkit.org/show_bug.cgi?id=267625 */
   aspect-ratio: 1;
   transform: translate(-50%, -50%);
   animation: isloadingspin 1000ms infinite linear;
@@ -29,10 +34,14 @@
   border-radius: var(--border-radius-circle);
 }
 
-.is-loading.small-loading-icon::after {
+.is-loading.loading-icon-2px::after {
   border-width: 2px;
 }
 
+.is-loading.loading-icon-3px::after {
+  border-width: 3px;
+}
+
 /* for single form button, the loading state should be on the button, but not go semi-transparent, just replace the text on the button with the loader. */
 form.single-button-form.is-loading > * {
   opacity: 1;
@@ -57,7 +66,7 @@ form.single-button-form.is-loading .button {
   background: transparent;
 }
 
-/* TODO: not needed, use "is-loading small-loading-icon" instead */
+/* TODO: not needed, use "is-loading loading-icon-2px" instead */
 code.language-math.is-loading::after {
   padding: 0;
   border-width: 2px;
diff --git a/web_src/css/modules/button.css b/web_src/css/modules/button.css
index 36cb499aeb..faeed8c9a1 100644
--- a/web_src/css/modules/button.css
+++ b/web_src/css/modules/button.css
@@ -61,11 +61,22 @@ It needs some tricks to tweak the left/right borders with active state */
   border-right: none;
 }
 
-.ui.buttons .button:first-child {
+.ui.buttons .button:hover {
+  border-color: var(--color-secondary-dark-2);
+}
+
+.ui.buttons .button:hover + .button {
+  border-left: 1px solid var(--color-secondary-dark-2);
+}
+
+/* TODO: these "tw-hidden" selectors are only used by "blame.tmpl" buttons: Raw/Normal View/History/Unescape, need to be refactored to a clear solution later */
+.ui.buttons .button:first-child,
+.ui.buttons .button.tw-hidden:first-child + .button {
   border-left: 1px solid var(--color-light-border);
 }
 
-.ui.buttons .button:last-child {
+.ui.buttons .button:last-child,
+.ui.buttons .button:nth-last-child(2):has(+ .button.tw-hidden) {
   border-right: 1px solid var(--color-light-border);
 }
 
@@ -85,6 +96,13 @@ It needs some tricks to tweak the left/right borders with active state */
   box-shadow: none;
 }
 
+/* apply the vertical padding of .compact to non-compact buttons when they contain a svg as they
+   would otherwise appear too large. Seen on "RSS Feed" button on repo releases tab. */
+.ui.small.button:not(.compact):has(.svg) {
+  padding-top: 0.58928571em;
+  padding-bottom: 0.58928571em;
+}
+
 .ui.labeled.button.disabled > .button,
 .ui.basic.buttons .button,
 .ui.basic.button,
@@ -98,6 +116,7 @@ It needs some tricks to tweak the left/right borders with active state */
 .ui.basic.button:hover {
   color: var(--color-text);
   background: var(--color-hover);
+  border-color: var(--color-secondary-dark-2);
 }
 
 .ui.basic.buttons .button:active,
diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css
new file mode 100644
index 0000000000..d3e45714a4
--- /dev/null
+++ b/web_src/css/modules/checkbox.css
@@ -0,0 +1,120 @@
+/* based on Fomantic UI checkbox module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+input[type="checkbox"],
+input[type="radio"] {
+  width: var(--checkbox-size);
+  height: var(--checkbox-size);
+}
+
+.ui.checkbox {
+  position: relative;
+  display: inline-block;
+  vertical-align: baseline;
+  min-height: var(--checkbox-size);
+  line-height: var(--checkbox-size);
+  min-width: var(--checkbox-size);
+  padding: 1px;
+}
+
+.ui.checkbox input[type="checkbox"],
+.ui.checkbox input[type="radio"] {
+  position: absolute;
+  top: 1px;
+  left: 0;
+  width: var(--checkbox-size);
+  height: var(--checkbox-size);
+}
+
+.ui.checkbox input[type="checkbox"]:enabled,
+.ui.checkbox input[type="radio"]:enabled,
+.ui.checkbox label:enabled {
+  cursor: pointer;
+}
+
+.ui.checkbox label {
+  cursor: auto;
+  position: relative;
+  display: block;
+  user-select: none;
+}
+
+.ui.checkbox label,
+.ui.radio.checkbox label {
+  margin-left: 1.85714em;
+}
+
+.ui.checkbox + label {
+  vertical-align: middle;
+}
+
+.ui.disabled.checkbox label,
+.ui.checkbox input[disabled] ~ label {
+  cursor: default !important;
+  opacity: 0.5;
+  pointer-events: none;
+}
+
+.ui.radio.checkbox {
+  min-height: var(--checkbox-size);
+}
+
+/* "switch" styled checkbox */
+
+.ui.toggle.checkbox {
+  min-height: 1.5rem;
+}
+.ui.toggle.checkbox input {
+  width: 3.5rem;
+  height: 1.5rem;
+  opacity: 0;
+  z-index: 3;
+}
+.ui.toggle.checkbox label {
+  min-height: 1.5rem;
+  padding-left: 4.5rem;
+  padding-top: 0.15em;
+}
+.ui.toggle.checkbox label::before {
+  display: block;
+  position: absolute;
+  content: "";
+  z-index: 1;
+  top: 0;
+  width: 3.5rem;
+  height: 1.5rem;
+  border-radius: 500rem;
+  left: 0;
+}
+.ui.toggle.checkbox label::after {
+  background: var(--color-white);
+  position: absolute;
+  content: "";
+  opacity: 1;
+  z-index: 2;
+  width: 1.5rem;
+  height: 1.5rem;
+  top: 0;
+  left: 0;
+  border-radius: 500rem;
+  transition: background 0.3s ease, left 0.3s ease;
+}
+.ui.toggle.checkbox input ~ label::after {
+  left: -0.05rem;
+}
+.ui.toggle.checkbox input:checked ~ label::after {
+  left: 2.15rem;
+}
+.ui.toggle.checkbox input:focus ~ label::before,
+.ui.toggle.checkbox label::before {
+  background: var(--color-input-toggle-background);
+}
+.ui.toggle.checkbox label,
+.ui.toggle.checkbox input:checked ~ label,
+.ui.toggle.checkbox input:focus:checked ~ label {
+  color: var(--color-text) !important;
+}
+.ui.toggle.checkbox input:checked ~ label::before,
+.ui.toggle.checkbox input:focus:checked ~ label::before {
+  background: var(--color-primary) !important;
+}
diff --git a/web_src/css/modules/container.css b/web_src/css/modules/container.css
new file mode 100644
index 0000000000..f394d6c06d
--- /dev/null
+++ b/web_src/css/modules/container.css
@@ -0,0 +1,59 @@
+/* based on Fomantic UI container module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.container {
+  display: block;
+  max-width: 100%;
+}
+
+@media (max-width: 767.98px) {
+  .ui.ui.ui.container:not(.fluid) {
+    width: auto;
+    margin-left: 1em;
+    margin-right: 1em;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991.98px) {
+  .ui.ui.ui.container:not(.fluid) {
+    width: 723px;
+    margin-left: auto;
+    margin-right: auto;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199.98px) {
+  .ui.ui.ui.container:not(.fluid) {
+    width: 933px;
+    margin-left: auto;
+    margin-right: auto;
+  }
+}
+
+@media (min-width: 1200px) {
+  .ui.ui.ui.container:not(.fluid) {
+    width: 1127px;
+    margin-left: auto;
+    margin-right: auto;
+  }
+}
+
+.ui.fluid.container {
+  width: 100%;
+}
+
+.ui[class*="center aligned"].container {
+  text-align: center;
+}
+
+/* overwrite width of containers inside the main page content div (div with class "page-content") */
+.page-content .ui.ui.ui.container:not(.fluid) {
+  width: 1280px;
+  max-width: calc(100% - calc(2 * var(--page-margin-x)));
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.ui.container.fluid.padded {
+  padding: 0 var(--page-margin-x);
+}
diff --git a/web_src/css/modules/flexcontainer.css b/web_src/css/modules/flexcontainer.css
index 0b559f1e7d..1ca513687f 100644
--- a/web_src/css/modules/flexcontainer.css
+++ b/web_src/css/modules/flexcontainer.css
@@ -2,7 +2,8 @@
 
 .flex-container {
   display: flex !important;
-  gap: 16px;
+  gap: var(--page-spacing);
+  margin-top: var(--page-spacing);
 }
 
 .flex-container-nav {
diff --git a/web_src/css/modules/grid.css b/web_src/css/modules/grid.css
new file mode 100644
index 0000000000..4aaa452372
--- /dev/null
+++ b/web_src/css/modules/grid.css
@@ -0,0 +1,516 @@
+/* based on Fomantic UI grid module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.grid {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  align-items: stretch;
+  padding: 0;
+  margin-top: -1rem;
+  margin-bottom: -1rem;
+  margin-left: -1rem;
+  margin-right: -1rem;
+}
+
+.ui.relaxed.grid {
+  margin-left: -1.5rem;
+  margin-right: -1.5rem;
+}
+.ui[class*="very relaxed"].grid {
+  margin-left: -2.5rem;
+  margin-right: -2.5rem;
+}
+
+.ui.grid + .grid {
+  margin-top: 1rem;
+}
+
+.ui.grid > .column:not(.row),
+.ui.grid > .row > .column {
+  position: relative;
+  display: inline-block;
+  width: 6.25%;
+  padding-left: 1rem;
+  padding-right: 1rem;
+  vertical-align: top;
+}
+.ui.grid > * {
+  padding-left: 1rem;
+  padding-right: 1rem;
+}
+
+.ui.grid > .row {
+  position: relative;
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  justify-content: inherit;
+  align-items: stretch;
+  width: 100% !important;
+  padding: 0;
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+}
+
+.ui.grid > .column:not(.row) {
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+}
+.ui.grid > .row > .column {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+.ui.grid > .row > img,
+.ui.grid > .row > .column > img {
+  max-width: 100%;
+}
+
+.ui.grid > .ui.grid:first-child {
+  margin-top: 0;
+}
+.ui.grid > .ui.grid:last-child {
+  margin-bottom: 0;
+}
+
+.ui.grid .aligned.row > .column > .segment:not(.compact):not(.attached),
+.ui.aligned.grid .column > .segment:not(.compact):not(.attached) {
+  width: 100%;
+}
+
+.ui.grid .row + .ui.divider {
+  flex-grow: 1;
+  margin: 1rem;
+}
+.ui.grid .column + .ui.vertical.divider {
+  height: calc(50% - 1rem);
+}
+
+.ui.grid > .row > .column:last-child > .horizontal.segment,
+.ui.grid > .column:last-child > .horizontal.segment {
+  box-shadow: none;
+}
+
+@media only screen and (max-width: 767.98px) {
+  .ui.page.grid {
+    width: auto;
+    padding-left: 0;
+    padding-right: 0;
+    margin-left: 0;
+    margin-right: 0;
+  }
+}
+@media only screen and (min-width: 768px) and (max-width: 991.98px) {
+  .ui.page.grid {
+    width: auto;
+    margin-left: 0;
+    margin-right: 0;
+    padding-left: 2em;
+    padding-right: 2em;
+  }
+}
+@media only screen and (min-width: 992px) and (max-width: 1199.98px) {
+  .ui.page.grid {
+    width: auto;
+    margin-left: 0;
+    margin-right: 0;
+    padding-left: 3%;
+    padding-right: 3%;
+  }
+}
+@media only screen and (min-width: 1200px) and (max-width: 1919.98px) {
+  .ui.page.grid {
+    width: auto;
+    margin-left: 0;
+    margin-right: 0;
+    padding-left: 15%;
+    padding-right: 15%;
+  }
+}
+@media only screen and (min-width: 1920px) {
+  .ui.page.grid {
+    width: auto;
+    margin-left: 0;
+    margin-right: 0;
+    padding-left: 23%;
+    padding-right: 23%;
+  }
+}
+
+.ui.grid > .column:only-child,
+.ui.grid > .row > .column:only-child {
+  width: 100%;
+}
+
+.ui[class*="one column"].grid > .row > .column,
+.ui[class*="one column"].grid > .column:not(.row) {
+  width: 100%;
+}
+.ui[class*="two column"].grid > .row > .column,
+.ui[class*="two column"].grid > .column:not(.row) {
+  width: 50%;
+}
+.ui[class*="three column"].grid > .row > .column,
+.ui[class*="three column"].grid > .column:not(.row) {
+  width: 33.33333333%;
+}
+.ui[class*="four column"].grid > .row > .column,
+.ui[class*="four column"].grid > .column:not(.row) {
+  width: 25%;
+}
+.ui[class*="five column"].grid > .row > .column,
+.ui[class*="five column"].grid > .column:not(.row) {
+  width: 20%;
+}
+.ui[class*="six column"].grid > .row > .column,
+.ui[class*="six column"].grid > .column:not(.row) {
+  width: 16.66666667%;
+}
+.ui[class*="seven column"].grid > .row > .column,
+.ui[class*="seven column"].grid > .column:not(.row) {
+  width: 14.28571429%;
+}
+.ui[class*="eight column"].grid > .row > .column,
+.ui[class*="eight column"].grid > .column:not(.row) {
+  width: 12.5%;
+}
+.ui[class*="nine column"].grid > .row > .column,
+.ui[class*="nine column"].grid > .column:not(.row) {
+  width: 11.11111111%;
+}
+.ui[class*="ten column"].grid > .row > .column,
+.ui[class*="ten column"].grid > .column:not(.row) {
+  width: 10%;
+}
+.ui[class*="eleven column"].grid > .row > .column,
+.ui[class*="eleven column"].grid > .column:not(.row) {
+  width: 9.09090909%;
+}
+.ui[class*="twelve column"].grid > .row > .column,
+.ui[class*="twelve column"].grid > .column:not(.row) {
+  width: 8.33333333%;
+}
+.ui[class*="thirteen column"].grid > .row > .column,
+.ui[class*="thirteen column"].grid > .column:not(.row) {
+  width: 7.69230769%;
+}
+.ui[class*="fourteen column"].grid > .row > .column,
+.ui[class*="fourteen column"].grid > .column:not(.row) {
+  width: 7.14285714%;
+}
+.ui[class*="fifteen column"].grid > .row > .column,
+.ui[class*="fifteen column"].grid > .column:not(.row) {
+  width: 6.66666667%;
+}
+.ui[class*="sixteen column"].grid > .row > .column,
+.ui[class*="sixteen column"].grid > .column:not(.row) {
+  width: 6.25%;
+}
+
+.ui.grid > [class*="one column"].row > .column {
+  width: 100% !important;
+}
+.ui.grid > [class*="two column"].row > .column {
+  width: 50% !important;
+}
+.ui.grid > [class*="three column"].row > .column {
+  width: 33.33333333% !important;
+}
+.ui.grid > [class*="four column"].row > .column {
+  width: 25% !important;
+}
+.ui.grid > [class*="five column"].row > .column {
+  width: 20% !important;
+}
+.ui.grid > [class*="six column"].row > .column {
+  width: 16.66666667% !important;
+}
+.ui.grid > [class*="seven column"].row > .column {
+  width: 14.28571429% !important;
+}
+.ui.grid > [class*="eight column"].row > .column {
+  width: 12.5% !important;
+}
+.ui.grid > [class*="nine column"].row > .column {
+  width: 11.11111111% !important;
+}
+.ui.grid > [class*="ten column"].row > .column {
+  width: 10% !important;
+}
+.ui.grid > [class*="eleven column"].row > .column {
+  width: 9.09090909% !important;
+}
+.ui.grid > [class*="twelve column"].row > .column {
+  width: 8.33333333% !important;
+}
+.ui.grid > [class*="thirteen column"].row > .column {
+  width: 7.69230769% !important;
+}
+.ui.grid > [class*="fourteen column"].row > .column {
+  width: 7.14285714% !important;
+}
+.ui.grid > [class*="fifteen column"].row > .column {
+  width: 6.66666667% !important;
+}
+.ui.grid > [class*="sixteen column"].row > .column {
+  width: 6.25% !important;
+}
+
+.ui.grid > .row > [class*="one wide"].column,
+.ui.grid > .column.row > [class*="one wide"].column,
+.ui.grid > [class*="one wide"].column,
+.ui.column.grid > [class*="one wide"].column {
+  width: 6.25% !important;
+}
+.ui.grid > .row > [class*="two wide"].column,
+.ui.grid > .column.row > [class*="two wide"].column,
+.ui.grid > [class*="two wide"].column,
+.ui.column.grid > [class*="two wide"].column {
+  width: 12.5% !important;
+}
+.ui.grid > .row > [class*="three wide"].column,
+.ui.grid > .column.row > [class*="three wide"].column,
+.ui.grid > [class*="three wide"].column,
+.ui.column.grid > [class*="three wide"].column {
+  width: 18.75% !important;
+}
+.ui.grid > .row > [class*="four wide"].column,
+.ui.grid > .column.row > [class*="four wide"].column,
+.ui.grid > [class*="four wide"].column,
+.ui.column.grid > [class*="four wide"].column {
+  width: 25% !important;
+}
+.ui.grid > .row > [class*="five wide"].column,
+.ui.grid > .column.row > [class*="five wide"].column,
+.ui.grid > [class*="five wide"].column,
+.ui.column.grid > [class*="five wide"].column {
+  width: 31.25% !important;
+}
+.ui.grid > .row > [class*="six wide"].column,
+.ui.grid > .column.row > [class*="six wide"].column,
+.ui.grid > [class*="six wide"].column,
+.ui.column.grid > [class*="six wide"].column {
+  width: 37.5% !important;
+}
+.ui.grid > .row > [class*="seven wide"].column,
+.ui.grid > .column.row > [class*="seven wide"].column,
+.ui.grid > [class*="seven wide"].column,
+.ui.column.grid > [class*="seven wide"].column {
+  width: 43.75% !important;
+}
+.ui.grid > .row > [class*="eight wide"].column,
+.ui.grid > .column.row > [class*="eight wide"].column,
+.ui.grid > [class*="eight wide"].column,
+.ui.column.grid > [class*="eight wide"].column {
+  width: 50% !important;
+}
+.ui.grid > .row > [class*="nine wide"].column,
+.ui.grid > .column.row > [class*="nine wide"].column,
+.ui.grid > [class*="nine wide"].column,
+.ui.column.grid > [class*="nine wide"].column {
+  width: 56.25% !important;
+}
+.ui.grid > .row > [class*="ten wide"].column,
+.ui.grid > .column.row > [class*="ten wide"].column,
+.ui.grid > [class*="ten wide"].column,
+.ui.column.grid > [class*="ten wide"].column {
+  width: 62.5% !important;
+}
+.ui.grid > .row > [class*="eleven wide"].column,
+.ui.grid > .column.row > [class*="eleven wide"].column,
+.ui.grid > [class*="eleven wide"].column,
+.ui.column.grid > [class*="eleven wide"].column {
+  width: 68.75% !important;
+}
+.ui.grid > .row > [class*="twelve wide"].column,
+.ui.grid > .column.row > [class*="twelve wide"].column,
+.ui.grid > [class*="twelve wide"].column,
+.ui.column.grid > [class*="twelve wide"].column {
+  width: 75% !important;
+}
+.ui.grid > .row > [class*="thirteen wide"].column,
+.ui.grid > .column.row > [class*="thirteen wide"].column,
+.ui.grid > [class*="thirteen wide"].column,
+.ui.column.grid > [class*="thirteen wide"].column {
+  width: 81.25% !important;
+}
+.ui.grid > .row > [class*="fourteen wide"].column,
+.ui.grid > .column.row > [class*="fourteen wide"].column,
+.ui.grid > [class*="fourteen wide"].column,
+.ui.column.grid > [class*="fourteen wide"].column {
+  width: 87.5% !important;
+}
+.ui.grid > .row > [class*="fifteen wide"].column,
+.ui.grid > .column.row > [class*="fifteen wide"].column,
+.ui.grid > [class*="fifteen wide"].column,
+.ui.column.grid > [class*="fifteen wide"].column {
+  width: 93.75% !important;
+}
+.ui.grid > .row > [class*="sixteen wide"].column,
+.ui.grid > .column.row > [class*="sixteen wide"].column,
+.ui.grid > [class*="sixteen wide"].column,
+.ui.column.grid > [class*="sixteen wide"].column {
+  width: 100% !important;
+}
+
+.ui.centered.grid,
+.ui.centered.grid > .row,
+.ui.grid > .centered.row {
+  text-align: center;
+  justify-content: center;
+}
+.ui.centered.grid > .column:not(.aligned):not(.justified):not(.row),
+.ui.centered.grid > .row > .column:not(.aligned):not(.justified),
+.ui.grid .centered.row > .column:not(.aligned):not(.justified) {
+  text-align: left;
+}
+.ui.grid > .centered.column,
+.ui.grid > .row > .centered.column {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.ui.relaxed.grid > .column:not(.row),
+.ui.relaxed.grid > .row > .column,
+.ui.grid > .relaxed.row > .column {
+  padding-left: 1.5rem;
+  padding-right: 1.5rem;
+}
+.ui[class*="very relaxed"].grid > .column:not(.row),
+.ui[class*="very relaxed"].grid > .row > .column,
+.ui.grid > [class*="very relaxed"].row > .column {
+  padding-left: 2.5rem;
+  padding-right: 2.5rem;
+}
+
+.ui.relaxed.grid .row + .ui.divider,
+.ui.grid .relaxed.row + .ui.divider {
+  margin-left: 1.5rem;
+  margin-right: 1.5rem;
+}
+.ui[class*="very relaxed"].grid .row + .ui.divider,
+.ui.grid [class*="very relaxed"].row + .ui.divider {
+  margin-left: 2.5rem;
+  margin-right: 2.5rem;
+}
+
+.ui[class*="middle aligned"].grid > .column:not(.row),
+.ui[class*="middle aligned"].grid > .row > .column,
+.ui.grid > [class*="middle aligned"].row > .column,
+.ui.grid > [class*="middle aligned"].column:not(.row),
+.ui.grid > .row > [class*="middle aligned"].column {
+  flex-direction: column;
+  vertical-align: middle;
+  align-self: center !important;
+}
+
+.ui[class*="left aligned"].grid > .column,
+.ui[class*="left aligned"].grid > .row > .column,
+.ui.grid > [class*="left aligned"].row > .column,
+.ui.grid > [class*="left aligned"].column.column,
+.ui.grid > .row > [class*="left aligned"].column.column {
+  text-align: left;
+  align-self: inherit;
+}
+
+.ui[class*="center aligned"].grid > .column,
+.ui[class*="center aligned"].grid > .row > .column,
+.ui.grid > [class*="center aligned"].row > .column,
+.ui.grid > [class*="center aligned"].column.column,
+.ui.grid > .row > [class*="center aligned"].column.column {
+  text-align: center;
+  align-self: inherit;
+}
+.ui[class*="center aligned"].grid {
+  justify-content: center;
+}
+
+.ui[class*="right aligned"].grid > .column,
+.ui[class*="right aligned"].grid > .row > .column,
+.ui.grid > [class*="right aligned"].row > .column,
+.ui.grid > [class*="right aligned"].column.column,
+.ui.grid > .row > [class*="right aligned"].column.column {
+  text-align: right;
+  align-self: inherit;
+}
+
+.ui[class*="equal width"].grid > .column:not(.row),
+.ui[class*="equal width"].grid > .row > .column,
+.ui.grid > [class*="equal width"].row > .column {
+  display: inline-block;
+  flex-grow: 1;
+}
+.ui[class*="equal width"].grid > .wide.column,
+.ui[class*="equal width"].grid > .row > .wide.column,
+.ui.grid > [class*="equal width"].row > .wide.column {
+  flex-grow: 0;
+}
+
+@media only screen and (max-width: 767.98px) {
+  .ui[class*="mobile reversed"].grid,
+  .ui[class*="mobile reversed"].grid > .row,
+  .ui.grid > [class*="mobile reversed"].row {
+    flex-direction: row-reverse;
+  }
+  .ui.stackable[class*="mobile reversed"] {
+    flex-direction: column-reverse;
+  }
+}
+
+@media only screen and (max-width: 767.98px) {
+  .ui.stackable.grid {
+    width: auto;
+    margin-left: 0 !important;
+    margin-right: 0 !important;
+  }
+  .ui.stackable.grid > .row > .wide.column,
+  .ui.stackable.grid > .wide.column,
+  .ui.stackable.grid > .column.grid > .column,
+  .ui.stackable.grid > .column.row > .column,
+  .ui.stackable.grid > .row > .column,
+  .ui.stackable.grid > .column:not(.row),
+  .ui.grid > .stackable.stackable.stackable.row > .column {
+    width: 100% !important;
+    margin: 0 !important;
+    box-shadow: none !important;
+    padding: 1rem;
+  }
+  .ui.stackable.grid:not(.vertically) > .row {
+    margin: 0;
+    padding: 0;
+  }
+
+  .ui.container > .ui.stackable.grid > .column,
+  .ui.container > .ui.stackable.grid > .row > .column {
+    padding-left: 0 !important;
+    padding-right: 0 !important;
+  }
+
+  .ui.grid .ui.stackable.grid,
+  .ui.segment:not(.vertical) .ui.stackable.page.grid {
+    margin-left: -1rem !important;
+    margin-right: -1rem !important;
+  }
+}
+
+.ui.ui.ui.compact.grid > .column:not(.row),
+.ui.ui.ui.compact.grid > .row > .column {
+  padding-left: 0.5rem;
+  padding-right: 0.5rem;
+}
+.ui.ui.ui.compact.grid > * {
+  padding-left: 0.5rem;
+  padding-right: 0.5rem;
+}
+
+.ui.ui.ui.compact.grid > .row {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
+
+.ui.ui.ui.compact.grid > .column:not(.row) {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
diff --git a/web_src/css/modules/header.css b/web_src/css/modules/header.css
new file mode 100644
index 0000000000..05381e1185
--- /dev/null
+++ b/web_src/css/modules/header.css
@@ -0,0 +1,177 @@
+/* based on Fomantic UI header module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.header {
+  color: var(--color-text);
+  border: none;
+  margin: calc(2rem - 0.1428571428571429em) 0 1rem;
+  padding: 0;
+  font-family: var(--fonts-regular);
+  font-weight: var(--font-weight-medium);
+  line-height: 1.28571429;
+  text-transform: none;
+}
+
+.ui.header:first-child {
+  margin-top: -0.14285714em;
+}
+
+.ui.header:last-child {
+  margin-bottom: 0;
+}
+
+.ui.header .ui.label {
+  margin-left: 0.25rem;
+  vertical-align: middle;
+}
+
+.ui.header > .ui.label.compact {
+  margin-top: inherit;
+}
+
+.ui.header .sub.header {
+  display: block;
+  font-weight: var(--font-weight-normal);
+  padding: 0;
+  margin: 0;
+  font-size: 1rem;
+  line-height: 1.2;
+  color: var(--color-text-light-1);
+}
+
+.ui.header > i.icon {
+  display: table-cell;
+  opacity: 1;
+  font-size: 1.5em;
+  padding-top: 0;
+  vertical-align: middle;
+}
+
+.ui.header > i.icon:only-child {
+  display: inline-block;
+  padding: 0;
+  margin-right: 0.75rem;
+}
+
+.ui.header + p {
+  margin-top: 0;
+}
+
+h2.ui.header {
+  font-size: 1.71428571rem;
+}
+h2.ui.header .sub.header {
+  font-size: 1.14285714rem;
+}
+
+h4.ui.header {
+  font-size: 1.07142857rem;
+}
+h4.ui.header .sub.header {
+  font-size: 1rem;
+}
+
+.ui.sub.header {
+  padding: 0;
+  margin-bottom: 0.14285714rem;
+  font-weight: var(--font-weight-normal);
+  font-size: 0.85714286em;
+}
+
+.ui.icon.header svg {
+  width: 3em;
+  height: 3em;
+  float: none;
+  display: block;
+  line-height: var(--line-height-default);
+  padding: 0;
+  margin: 0 auto 0.5rem;
+  opacity: 1;
+}
+
+.ui.header:not(h1,h2,h3,h4,h5,h6) {
+  font-size: 1.28571429em;
+}
+
+.ui.attached.header {
+  position: relative;
+  background: var(--color-box-header);
+  padding: 0.78571429rem 1rem;
+  margin: 0 -1px;
+  border-radius: 0;
+  border: 1px solid var(--color-secondary);
+}
+
+.ui.attached:not(.top).header {
+  border-top: none;
+}
+
+.ui.top.attached.header {
+  border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.bottom.attached.header {
+  border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+.ui.attached.header:not(h1,h2,h3,h4,h5,h6) {
+  font-size: 1em;
+}
+
+/* fix misaligned right buttons on box headers */
+.ui.attached.header > .ui.right {
+  position: absolute;
+  right: 0.78571429rem;
+  top: 0;
+  bottom: 0;
+  display: flex;
+  align-items: center;
+  gap: 0.25em;
+}
+
+/* the default ".ui.attached.header > .ui.right" is only able to contain "tiny" buttons, other buttons are too large */
+.ui.attached.header > .ui.right .ui.tiny.button {
+  padding: 6px 10px;
+  font-weight: var(--font-weight-normal);
+}
+
+/* open dropdown menus to the left in right-attached headers */
+.ui.attached.header > .ui.right .ui.dropdown .menu {
+  right: 0;
+  left: auto;
+}
+
+/* if a .top.attached.header is followed by a .segment, add some margin */
+.ui.segments + .ui.top.attached.header,
+.ui.attached.segment + .ui.top.attached.header {
+  margin-top: 1rem;
+}
+
+.ui.dividing.header {
+  border-bottom-color: var(--color-secondary);
+}
+
+.ui.dividing.header .sub.header {
+  padding-bottom: 0.21428571rem;
+}
+
+.ui.dividing.header i.icon {
+  margin-bottom: 0;
+}
+
+.ui.error.header {
+  background: var(--color-error-bg) !important;
+  color: var(--color-error-text) !important;
+  border-color: var(--color-error-border) !important;
+}
+
+.ui.warning.header {
+  background: var(--color-warning-bg) !important;
+  color: var(--color-warning-text) !important;
+  border-color: var(--color-warning-border) !important;
+}
+
+.attention-header {
+  padding: 0.5em 0.75em !important;
+  color: var(--color-text) !important;
+}
diff --git a/web_src/css/modules/input.css b/web_src/css/modules/input.css
new file mode 100644
index 0000000000..18b785ac82
--- /dev/null
+++ b/web_src/css/modules/input.css
@@ -0,0 +1,197 @@
+/* based on Fomantic UI input module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.input {
+  position: relative;
+  font-weight: var(--font-weight-normal);
+  display: inline-flex;
+  color: var(--color-input-text);
+}
+.ui.input > input {
+  margin: 0;
+  max-width: 100%;
+  flex: 1 0 auto;
+  outline: none;
+  font-family: var(--fonts-regular);
+  padding: 0.67857143em 1em;
+  border: 1px solid var(--color-input-border);
+  color: var(--color-input-text);
+  border-radius: 0.28571429rem;
+  line-height: var(--line-height-default);
+  text-align: start;
+}
+
+.ui.disabled.input,
+.ui.input:not(.disabled) input[disabled] {
+  opacity: var(--opacity-disabled);
+}
+.ui.disabled.input > input,
+.ui.input:not(.disabled) input[disabled] {
+  pointer-events: none;
+}
+
+.ui.input.focus > input,
+.ui.input > input:focus {
+  border-color: var(--color-primary);
+}
+
+.ui.input.error > input {
+  background: var(--color-error-bg);
+  border-color: var(--color-error-border);
+  color: var(--color-error-text);
+}
+
+.ui.icon.input > i.icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: default;
+  position: absolute;
+  text-align: center;
+  top: 0;
+  right: 0;
+  margin: 0;
+  height: 100%;
+  width: 2.67142857em;
+  opacity: 0.5;
+  border-radius: 0 0.28571429rem 0.28571429rem 0;
+  pointer-events: none;
+  padding: 4px;
+}
+
+.ui.icon.input > i.icon.is-loading {
+  position: absolute !important;
+  height: 28px;
+  top: 4px;
+}
+
+.ui.icon.input > i.icon.is-loading > * {
+  visibility: hidden;
+}
+
+.ui.ui.ui.ui.icon.input > textarea,
+.ui.ui.ui.ui.icon.input > input {
+  padding-right: 2.67142857em;
+}
+.ui.icon.input > i.link.icon {
+  cursor: pointer;
+}
+.ui.icon.input > i.circular.icon {
+  top: 0.35em;
+  right: 0.5em;
+}
+
+.ui[class*="left icon"].input > i.icon {
+  right: auto;
+  left: 1px;
+  border-radius: 0.28571429rem 0 0 0.28571429rem;
+}
+.ui[class*="left icon"].input > i.circular.icon {
+  right: auto;
+  left: 0.5em;
+}
+.ui.ui.ui.ui[class*="left icon"].input > textarea,
+.ui.ui.ui.ui[class*="left icon"].input > input {
+  padding-left: 2.67142857em;
+  padding-right: 1em;
+}
+
+.ui.icon.input > textarea:focus ~ .icon,
+.ui.icon.input > input:focus ~ .icon {
+  opacity: 1;
+}
+
+.ui.icon.input > textarea ~ i.icon {
+  height: 3em;
+}
+
+.ui.form .field.error > .ui.action.input > .ui.button,
+.ui.action.input.error > .ui.button {
+  border-top: 1px solid var(--color-error-border);
+  border-bottom: 1px solid var(--color-error-border);
+}
+
+.ui.action.input > .button,
+.ui.action.input > .buttons {
+  display: flex;
+  align-items: center;
+  flex: 0 0 auto;
+}
+.ui.action.input > .button,
+.ui.action.input > .buttons > .button {
+  padding-top: 0.78571429em;
+  padding-bottom: 0.78571429em;
+  margin: 0;
+}
+
+.ui.action.input:not([class*="left action"]) > input {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+  border-right-color: transparent;
+}
+
+.ui.action.input > .dropdown:first-child,
+.ui.action.input > .button:first-child,
+.ui.action.input > .buttons:first-child > .button {
+  border-radius: 0.28571429rem 0 0 0.28571429rem;
+}
+.ui.action.input > .dropdown:not(:first-child),
+.ui.action.input > .button:not(:first-child),
+.ui.action.input > .buttons:not(:first-child) > .button {
+  border-radius: 0;
+}
+.ui.action.input > .dropdown:last-child,
+.ui.action.input > .button:last-child,
+.ui.action.input > .buttons:last-child > .button {
+  border-radius: 0 0.28571429rem 0.28571429rem 0;
+}
+
+.ui.fluid.input {
+  display: flex;
+}
+.ui.fluid.input > input {
+  width: 0 !important;
+}
+
+.ui.tiny.input {
+  font-size: 0.85714286em;
+}
+.ui.small.input {
+  font-size: 0.92857143em;
+}
+
+.ui.action.input .ui.ui.button {
+  border-color: var(--color-input-border);
+  padding-top: 0; /* the ".action.input" is "flex + stretch", so let the buttons layout themselves */
+  padding-bottom: 0;
+}
+
+/* currently used for search bar dropdowns in repo search and explore code */
+.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection {
+  min-width: 10em;
+}
+.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus) {
+  border-right: none;
+}
+.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(.active):hover {
+  border-color: var(--color-input-border);
+}
+.ui.action.input:not([class*="left action"]) .ui.dropdown.selection.upward.visible {
+  border-bottom-left-radius: 0 !important;
+  border-bottom-right-radius: 0 !important;
+}
+.ui.action.input:not([class*="left action"]) > input,
+.ui.action.input:not([class*="left action"]) > input:hover {
+  border-right: none;
+}
+.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection,
+.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover,
+.ui.action.input:not([class*="left action"]) > input:focus + .button,
+.ui.action.input:not([class*="left action"]) > input:focus + .button:hover,
+.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button,
+.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover {
+  border-left-color: var(--color-primary);
+}
+.ui.action.input:not([class*="left action"]) > input:focus {
+  border-right-color: var(--color-primary);
+}
diff --git a/web_src/css/modules/label.css b/web_src/css/modules/label.css
new file mode 100644
index 0000000000..0512c5fddb
--- /dev/null
+++ b/web_src/css/modules/label.css
@@ -0,0 +1,294 @@
+/* based on Fomantic UI label module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.label {
+  display: inline-flex;
+  align-items: center;
+  gap: .25rem;
+  vertical-align: middle;
+  line-height: 1;
+  background: var(--color-label-bg);
+  color: var(--color-label-text);
+  padding: 0.3em 0.5em;
+  text-transform: none;
+  font-size: 0.85714286rem;
+  font-weight: var(--font-weight-medium);
+  border: 0 solid transparent;
+  border-radius: 0.28571429rem;
+  white-space: nowrap;
+}
+
+.ui.label:first-child {
+  margin-left: 0;
+}
+.ui.label:last-child {
+  margin-right: 0;
+}
+
+a.ui.label {
+  cursor: pointer;
+}
+
+.ui.label > a {
+  cursor: pointer;
+  color: inherit;
+  opacity: 0.75;
+}
+.ui.label > a:hover {
+  opacity: 1;
+}
+
+.ui.label > img {
+  width: auto;
+  vertical-align: middle;
+  height: 2.1666em;
+}
+
+.ui.label > .color-icon {
+  margin-left: 0;
+}
+
+.ui.label > .icon {
+  width: auto;
+  margin: 0 0.75em 0 0;
+}
+
+.ui.label > .detail {
+  display: inline-block;
+  vertical-align: top;
+  font-weight: var(--font-weight-medium);
+  margin-left: 1em;
+  opacity: 0.8;
+}
+.ui.label > .detail .icon {
+  margin: 0 0.25em 0 0;
+}
+
+.ui.label > .close.icon,
+.ui.label > .delete.icon {
+  cursor: pointer;
+  font-size: 0.92857143em;
+  opacity: 0.5;
+}
+.ui.label > .close.icon:hover,
+.ui.label > .delete.icon:hover {
+  opacity: 1;
+}
+
+.ui.label.left.icon > .close.icon,
+.ui.label.left.icon > .delete.icon {
+  margin: 0 0.5em 0 0;
+}
+.ui.label:not(.icon) > .close.icon,
+.ui.label:not(.icon) > .delete.icon {
+  margin: 0 0 0 0.5em;
+}
+
+.ui.header > .ui.label {
+  margin-top: -0.29165em;
+}
+
+a.ui.label:hover {
+  background: var(--color-label-hover-bg);
+  border-color: var(--color-label-hover-bg);
+  color: var(--color-label-text);
+}
+
+.ui.label.visible:not(.dropdown) {
+  display: inline-block !important;
+}
+
+.ui.basic.label {
+  background: var(--color-button);
+  border: 1px solid var(--color-light-border);
+  color: var(--color-text-light);
+  padding: calc(0.5833em - 1px) calc(0.833em - 1px);
+}
+a.ui.basic.label:hover {
+  text-decoration: none;
+  color: var(--color-text);
+  border-color: var(--color-light-border);
+  background: var(--color-hover);
+}
+
+.ui.ui.ui.primary.label {
+  background: var(--color-primary);
+  border-color: var(--color-primary-dark-2);
+  color: var(--color-primary-contrast);
+}
+a.ui.ui.ui.primary.label:hover {
+  background: var(--color-primary-dark-3);
+  border-color: var(--color-primary-dark-3);
+  color: var(--color-primary-contrast);
+}
+.ui.ui.ui.basic.primary.label {
+  background: transparent;
+  border-color: var(--color-primary);
+  color: var(--color-primary);
+}
+a.ui.ui.ui.basic.primary.label:hover {
+  background: var(--color-hover);
+  border-color: var(--color-primary-dark-1);
+  color: var(--color-primary-dark-1);
+}
+
+.ui.ui.ui.red.label {
+  background: var(--color-red);
+  border-color: var(--color-red);
+  color: var(--color-white);
+}
+a.ui.ui.ui.red.label:hover {
+  background: var(--color-red-dark-1);
+  border-color: var(--color-red-dark-1);
+  color: var(--color-white);
+}
+.ui.ui.ui.basic.red.label {
+  background: transparent;
+  border-color: var(--color-red);
+  color: var(--color-red);
+}
+a.ui.ui.ui.basic.red.label:hover {
+  background: transparent;
+  border-color: var(--color-red-dark-1);
+  color: var(--color-red-dark-1);
+}
+
+.ui.ui.ui.orange.label {
+  background: var(--color-orange);
+  border-color: var(--color-orange);
+  color: var(--color-white);
+}
+a.ui.ui.ui.orange.label:hover {
+  background: var(--color-orange-dark-1);
+  border-color: var(--color-orange-dark-1);
+  color: var(--color-white);
+}
+.ui.ui.ui.basic.orange.label {
+  background: transparent;
+  border-color: var(--color-orange);
+  color: var(--color-orange);
+}
+a.ui.ui.ui.basic.orange.label:hover {
+  background: transparent;
+  border-color: var(--color-orange-dark-1);
+  color: var(--color-orange-dark-1);
+}
+
+.ui.ui.ui.yellow.label {
+  background: var(--color-yellow);
+  border-color: var(--color-yellow);
+  color: var(--color-white);
+}
+a.ui.ui.ui.yellow.label:hover {
+  background: var(--color-yellow-dark-1);
+  border-color: var(--color-yellow-dark-1);
+  color: var(--color-white);
+}
+.ui.ui.ui.basic.yellow.label {
+  background: transparent;
+  border-color: var(--color-yellow);
+  color: var(--color-yellow);
+}
+a.ui.ui.ui.basic.yellow.label:hover {
+  background: transparent;
+  border-color: var(--color-yellow-dark-1);
+  color: var(--color-yellow-dark-1);
+}
+.ui.ui.ui.olive.label {
+  background: var(--color-olive);
+  border-color: var(--color-olive);
+  color: var(--color-white);
+}
+
+.ui.ui.ui.green.label {
+  background: var(--color-green);
+  border-color: var(--color-green);
+  color: var(--color-white);
+}
+a.ui.ui.ui.green.label:hover {
+  background: var(--color-green-dark-1);
+  border-color: var(--color-green-dark-1);
+  color: var(--color-white);
+}
+.ui.ui.ui.basic.green.label {
+  background: transparent;
+  border-color: var(--color-green);
+  color: var(--color-green);
+}
+a.ui.ui.ui.basic.green.label:hover {
+  background: transparent;
+  border-color: var(--color-green-dark-1);
+  color: var(--color-green-dark-1);
+}
+
+.ui.ui.ui.purple.label {
+  background: var(--color-purple);
+  border-color: var(--color-purple);
+  color: var(--color-white);
+}
+a.ui.ui.ui.purple.label:hover {
+  background: var(--color-purple-dark-1);
+  border-color: var(--color-purple-dark-1);
+  color: var(--color-white);
+}
+.ui.ui.ui.basic.purple.label {
+  background: transparent;
+  border-color: var(--color-purple);
+  color: var(--color-purple);
+}
+a.ui.ui.ui.basic.purple.label:hover {
+  background: transparent;
+  border-color: var(--color-purple-dark-1);
+  color: var(--color-purple-dark-1);
+}
+
+.ui.ui.ui.grey.label {
+  background: var(--color-label-bg);
+  border-color: var(--color-label-bg);
+  color: var(--color-label-text);
+}
+a.ui.ui.ui.grey.label:hover {
+  background: var(--color-label-hover-bg);
+  border-color: var(--color-label-hover-bg);
+  color: var(--color-white);
+}
+.ui.ui.ui.basic.grey.label {
+  background: transparent;
+  border-color: var(--color-label-bg);
+  color: var(--color-label-text);
+}
+a.ui.ui.ui.basic.grey.label:hover {
+  background: transparent;
+  border-color: var(--color-label-hover-bg);
+  color: var(--color-label-hover-bg);
+}
+
+.ui.horizontal.label {
+  margin: 0 0.5em 0 0;
+  padding: 0.4em 0.833em;
+  min-width: 3em;
+  text-align: center;
+}
+
+.ui.circular.label {
+  min-width: 2em;
+  min-height: 2em;
+  padding: 0.5em !important;
+  line-height: 1;
+  text-align: center;
+  border-radius: 500rem;
+  justify-content: center;
+}
+
+.ui.mini.label {
+  font-size: 0.64285714rem;
+}
+.ui.tiny.label {
+  font-size: 0.71428571rem;
+}
+.ui.small.label {
+  font-size: 0.78571429rem;
+}
+.ui.large.label {
+  font-size: 1rem;
+}
diff --git a/web_src/css/modules/list.css b/web_src/css/modules/list.css
new file mode 100644
index 0000000000..32c71e802b
--- /dev/null
+++ b/web_src/css/modules/list.css
@@ -0,0 +1,193 @@
+/* based on Fomantic UI list module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.list {
+  list-style-type: none;
+  margin: 1em 0;
+  padding: 0;
+  font-size: 1em;
+}
+
+.ui.list:first-child {
+  margin-top: 0;
+  padding-top: 0;
+}
+
+.ui.list:last-child {
+  margin-bottom: 0;
+  padding-bottom: 0;
+}
+
+.ui.list > .item,
+.ui.list .list > .item {
+  display: list-item;
+  table-layout: fixed;
+  list-style-type: none;
+  list-style-position: outside;
+}
+
+.ui.list > .list > .item::after,
+.ui.list > .item::after {
+  content: "";
+  display: block;
+  height: 0;
+  clear: both;
+  visibility: hidden;
+}
+
+.ui.list .list:not(.icon) {
+  clear: both;
+  margin: 0;
+  padding: 0.75em 0 0.25em 0.5em;
+}
+
+.ui.list .list > .item {
+  padding: 0.14285714em 0;
+}
+
+.ui.list .list > .item > i.icon,
+.ui.list > .item > i.icon {
+  display: table-cell;
+  min-width: 1.55em;
+  padding-top: 0;
+  transition: color 0.1s ease;
+  padding-right: 0.28571429em;
+  vertical-align: top;
+}
+.ui.list .list > .item > i.icon:only-child,
+.ui.list > .item > i.icon:only-child {
+  display: inline-block;
+  min-width: auto;
+  vertical-align: top;
+}
+
+.ui.list .list > .item > .image,
+.ui.list > .item > .image {
+  display: table-cell;
+  background-color: transparent;
+  vertical-align: top;
+}
+.ui.list .list > .item > .image:not(:only-child):not(img),
+.ui.list > .item > .image:not(:only-child):not(img) {
+  padding-right: 0.5em;
+}
+.ui.list .list > .item > .image img,
+.ui.list > .item > .image img {
+  vertical-align: top;
+}
+.ui.list .list > .item > img.image,
+.ui.list .list > .item > .image:only-child,
+.ui.list > .item > img.image,
+.ui.list > .item > .image:only-child {
+  display: inline-block;
+}
+
+.ui.list .list > .item > .content,
+.ui.list > .item > .content {
+  color: var(--color-text);
+}
+.ui.list .list > .item > .image + .content,
+.ui.list .list > .item > i.icon + .content,
+.ui.list > .item > .image + .content,
+.ui.list > .item > i.icon + .content {
+  display: table-cell;
+  width: 100%;
+  padding: 0 0 0 0.5em;
+  vertical-align: top;
+}
+.ui.list .list > .item > img.image + .content,
+.ui.list > .item > img.image + .content {
+  display: inline-block;
+  width: auto;
+}
+.ui.list .list > .item > .content > .list,
+.ui.list > .item > .content > .list {
+  margin-left: 0;
+  padding-left: 0;
+}
+
+.ui.list .list > .item .header,
+.ui.list > .item .header {
+  display: block;
+  margin: 0;
+  font-family: var(--fonts-regular);
+  font-weight: var(--font-weight-medium);
+  color: var(--color-text-dark);
+}
+
+.ui.list .list > .item .description,
+.ui.list > .item .description {
+  display: block;
+  color: var(--color-text);
+}
+
+.ui.list > .item a,
+.ui.list .list > .item a {
+  cursor: pointer;
+}
+
+.ui.list .list > .item [class*="right floated"],
+.ui.list > .item [class*="right floated"] {
+  float: right;
+  margin: 0 0 0 1em;
+}
+
+.ui.menu .ui.list > .item,
+.ui.menu .ui.list .list > .item {
+  display: list-item;
+  table-layout: fixed;
+  background-color: transparent;
+  list-style-type: none;
+  list-style-position: outside;
+  padding: 0.21428571em 0;
+}
+.ui.menu .ui.list .list > .item::before,
+.ui.menu .ui.list > .item::before {
+  border: none;
+  background: none;
+}
+.ui.menu .ui.list .list > .item:first-child,
+.ui.menu .ui.list > .item:first-child {
+  padding-top: 0;
+}
+.ui.menu .ui.list .list > .item:last-child,
+.ui.menu .ui.list > .item:last-child {
+  padding-bottom: 0;
+}
+
+.ui.list .list > .disabled.item,
+.ui.list > .disabled.item {
+  pointer-events: none;
+  opacity: var(--opacity-disabled);
+}
+
+.ui.list .list > a.item:hover > .icons,
+.ui.list > a.item:hover > .icons,
+.ui.list .list > a.item:hover > i.icon,
+.ui.list > a.item:hover > i.icon {
+  color: var(--color-text-dark);
+}
+
+.ui.divided.list > .item {
+  border-top: 1px solid var(--color-secondary);
+}
+.ui.divided.list .list > .item {
+  border-top: none;
+}
+.ui.divided.list .item .list > .item {
+  border-top: none;
+}
+.ui.divided.list .list > .item:first-child,
+.ui.divided.list > .item:first-child {
+  border-top: none;
+}
+.ui.divided.list .list > .item:first-child {
+  border-top-width: 1px;
+}
+
+.ui.relaxed.list > .item:not(:first-child) {
+  padding-top: 0.42857143em;
+}
+.ui.relaxed.list > .item:not(:last-child) {
+  padding-bottom: 0.42857143em;
+}
diff --git a/web_src/css/modules/message.css b/web_src/css/modules/message.css
new file mode 100644
index 0000000000..c62dbddd25
--- /dev/null
+++ b/web_src/css/modules/message.css
@@ -0,0 +1,114 @@
+/* based on Fomantic UI message module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.message {
+  background: var(--color-box-body);
+  color: var(--color-text);
+  border: 1px solid var(--color-secondary);
+  position: relative;
+  min-height: 1em;
+  margin: 1em 0;
+  padding: 1em 1.5em;
+  border-radius: var(--border-radius);
+}
+
+.ui.message:first-child {
+  margin-top: 0;
+}
+
+.ui.message:last-child {
+  margin-bottom: 0;
+}
+
+.ui.attached.message {
+  margin-bottom: -1px;
+  border-radius: var(--border-radius) var(--border-radius) 0 0;
+  margin-left: -1px;
+  margin-right: -1px;
+}
+
+.ui.attached + .ui.attached.message:not(.top):not(.bottom) {
+  margin-top: -1px;
+  border-radius: 0;
+}
+
+.ui.bottom.attached.message {
+  margin-top: -1px;
+  border-radius: 0 0 var(--border-radius) var(--border-radius);
+}
+
+.ui.bottom.attached.message:not(:last-child) {
+  margin-bottom: 1em;
+}
+
+.ui.info.message .header,
+.ui.blue.message .header {
+  color: var(--color-blue);
+}
+
+.ui.info.message,
+.ui.attached.info.message,
+.ui.blue.message,
+.ui.attached.blue.message {
+  background: var(--color-info-bg);
+  color: var(--color-info-text);
+  border-color: var(--color-info-border);
+}
+
+.ui.success.message .header,
+.ui.positive.message .header,
+.ui.green.message .header {
+  color: var(--color-green);
+}
+
+.ui.success.message,
+.ui.attached.success.message,
+.ui.positive.message,
+.ui.attached.positive.message {
+  background: var(--color-success-bg);
+  color: var(--color-success-text);
+  border-color: var(--color-success-border);
+}
+
+.ui.error.message .header,
+.ui.negative.message .header,
+.ui.red.message .header {
+  color: var(--color-red);
+}
+
+.ui.error.message,
+.ui.attached.error.message,
+.ui.red.message,
+.ui.attached.red.message,
+.ui.negative.message,
+.ui.attached.negative.message {
+  background: var(--color-error-bg);
+  color: var(--color-error-text);
+  border-color: var(--color-error-border);
+}
+
+.ui.warning.message .header,
+.ui.yellow.message .header {
+  color: var(--color-yellow);
+}
+
+.ui.warning.message,
+.ui.attached.warning.message,
+.ui.yellow.message,
+.ui.attached.yellow.message {
+  background: var(--color-warning-bg);
+  color: var(--color-warning-text);
+  border-color: var(--color-warning-border);
+}
+
+.ui.message > .close.icon {
+  cursor: pointer;
+  position: absolute;
+  top: 9px;
+  right: 9px;
+  opacity: .7;
+}
+
+.ui.message > .close.icon:hover {
+  opacity: 1;
+}
diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css
index b6fd2ff20a..d7aa197e02 100644
--- a/web_src/css/modules/navbar.css
+++ b/web_src/css/modules/navbar.css
@@ -108,13 +108,6 @@
   }
 }
 
-@media (min-width: 767.98px) {
-  #navbar .navbar-mobile-right,
-  #navbar .mobile-only {
-    display: none;
-  }
-}
-
 #navbar a.item .notification_count {
   color: var(--color-nav-bg);
   padding: 0 3.75px;
@@ -143,3 +136,12 @@
   justify-content: center;
   z-index: 1; /* prevent menu button background from overlaying icon */
 }
+
+.secondary-nav {
+  background: var(--color-secondary-nav-bg) !important; /* important because of .ui.secondary.menu */
+}
+
+.issue-navbar {
+  display: flex;
+  justify-content: space-between;
+}
diff --git a/web_src/css/modules/segment.css b/web_src/css/modules/segment.css
new file mode 100644
index 0000000000..bbd39c385f
--- /dev/null
+++ b/web_src/css/modules/segment.css
@@ -0,0 +1,196 @@
+/* based on Fomantic UI segment module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.segment {
+  position: relative;
+  margin: 1rem 0;
+  padding: 1em;
+  border-radius: 0.28571429rem;
+  border: 1px solid var(--color-secondary);
+  background: var(--color-box-body);
+  color: var(--color-text);
+}
+.ui.segment:first-child {
+  margin-top: 0;
+}
+.ui.segment:last-child {
+  margin-bottom: 0;
+}
+
+.ui.grid.segment {
+  margin: 1rem 0;
+  border-radius: 0.28571429rem;
+}
+
+.ui.segment.tab:last-child {
+  margin-bottom: 1rem;
+}
+
+.ui.segments {
+  flex-direction: column;
+  position: relative;
+  margin: 1rem 0;
+  border: 1px solid var(--color-secondary);
+  border-radius: 0.28571429rem;
+  background: var(--color-box-body);
+  color: var(--color-text);
+}
+.ui.segments:first-child {
+  margin-top: 0;
+}
+.ui.segments:last-child {
+  margin-bottom: 0;
+}
+
+.ui.segments > .segment {
+  top: 0;
+  bottom: 0;
+  border-radius: 0;
+  margin: 0;
+  width: auto;
+  box-shadow: none;
+  border: none;
+  border-top: 1px solid var(--color-secondary);
+}
+.ui.segments:not(.horizontal) > .segment:first-child {
+  top: 0;
+  bottom: 0;
+  border-top: none;
+  margin-top: 0;
+  margin-bottom: 0;
+  border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.segments:not(.horizontal) > .segment:last-child {
+  top: 0;
+  bottom: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+  border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+.ui.segments:not(.horizontal) > .segment:only-child,
+.ui.segments:not(.horizontal) > .segment:has(~ .tw-hidden) { /* workaround issue with :last-child ignoring hidden elements */
+  border-radius: 0.28571429rem;
+}
+
+.ui.segments > .ui.segments {
+  border-top: 1px solid var(--color-secondary);
+  margin: 1rem;
+}
+.ui.segments > .segments:first-child {
+  border-top: none;
+}
+.ui.segments > .segment + .segments:not(.horizontal) {
+  margin-top: 0;
+}
+
+.ui.horizontal.segments {
+  display: flex;
+  flex-direction: row;
+  background-color: transparent;
+  padding: 0;
+  margin: 1rem 0;
+  border-radius: 0.28571429rem;
+  border: 1px solid var(--color-secondary);
+}
+
+.ui.horizontal.segments > .segment {
+  margin: 0;
+  min-width: 0;
+  border-radius: 0;
+  border: none;
+  box-shadow: none;
+  border-left: 1px solid var(--color-secondary);
+}
+
+.ui.segments > .horizontal.segments:first-child {
+  border-top: none;
+}
+.ui.horizontal.segments:not(.stackable) > .segment:first-child {
+  border-left: none;
+}
+.ui.horizontal.segments > .segment:first-child {
+  border-radius: 0.28571429rem 0 0 0.28571429rem;
+}
+.ui.horizontal.segments > .segment:last-child {
+  border-radius: 0 0.28571429rem 0.28571429rem 0;
+}
+
+.ui.clearing.segment::after {
+  content: "";
+  display: block;
+  clear: both;
+}
+
+.ui[class*="left aligned"].segment {
+  text-align: left;
+}
+.ui[class*="center aligned"].segment {
+  text-align: center;
+}
+
+.ui.secondary.segment {
+  background: var(--color-secondary-bg);
+  color: var(--color-text-light);
+}
+
+.ui.attached.segment {
+  top: 0;
+  bottom: 0;
+  border-radius: 0;
+  margin: 0 -1px;
+  width: calc(100% + 2px);
+  max-width: calc(100% + 2px);
+  box-shadow: none;
+  border: 1px solid var(--color-secondary);
+  background: var(--color-box-body);
+  color: var(--color-text);
+}
+.ui.attached:not(.message) + .ui.attached.segment:not(.top) {
+  border-top: none;
+}
+
+.ui[class*="top attached"].segment {
+  bottom: 0;
+  margin-bottom: 0;
+  top: 0;
+  margin-top: 1rem;
+  border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+.ui.segment[class*="top attached"]:first-child {
+  margin-top: 0;
+}
+
+.ui.segment[class*="bottom attached"] {
+  bottom: 0;
+  margin-top: 0;
+  top: 0;
+  margin-bottom: 1rem;
+  border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+.ui.segment[class*="bottom attached"]:last-child {
+  margin-bottom: 1rem;
+}
+
+.ui.fitted.segment:not(.horizontally) {
+  padding-top: 0;
+  padding-bottom: 0;
+}
+.ui.fitted.segment:not(.vertically) {
+  padding-left: 0;
+  padding-right: 0;
+}
+
+.ui.segments .segment,
+.ui.segment {
+  font-size: 1rem;
+}
+
+.ui.error.segment {
+  border-color: var(--color-error-border) !important;
+}
+
+.ui.warning.segment {
+  border-color: var(--color-warning-border) !important;
+}
diff --git a/web_src/css/modules/table.css b/web_src/css/modules/table.css
new file mode 100644
index 0000000000..4fb9d4214e
--- /dev/null
+++ b/web_src/css/modules/table.css
@@ -0,0 +1,385 @@
+/* based on Fomantic UI segment module, with just the parts extracted that we use. If you find any
+   unused rules here after refactoring, please remove them. */
+
+.ui.table {
+  width: 100%;
+  margin: 1em 0;
+  border: 1px solid var(--color-secondary);
+  border-radius: 0.28571429rem;
+  vertical-align: middle;
+  border-collapse: separate;
+  border-spacing: 0;
+  color: var(--color-text);
+  background: var(--color-box-body);
+  border-color: var(--color-secondary);
+  text-align: start;
+}
+
+.ui.table:first-child {
+  margin-top: 0;
+}
+.ui.table:last-child {
+  margin-bottom: 0;
+}
+.ui.table > thead,
+.ui.table > tbody {
+  text-align: inherit;
+  vertical-align: inherit;
+}
+
+.ui.table > thead > tr > th {
+  background: var(--color-box-header);
+  text-align: inherit;
+  color: var(--color-text);
+  padding: 6px 5px;
+  vertical-align: inherit;
+  font-weight: var(--font-weight-normal);
+  border-bottom: 1px solid var(--color-secondary);
+  border-left: none;
+}
+.ui.table > thead > tr > th:first-child {
+  border-left: none;
+}
+.ui.table > thead > tr:first-child > th:first-child {
+  border-radius: 0.28571429rem 0 0;
+}
+.ui.table > thead > tr:first-child > th:last-child {
+  border-radius: 0 0.28571429rem 0 0;
+}
+.ui.table > thead > tr:first-child > th:only-child {
+  border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.table > tfoot > tr > th,
+.ui.table > tfoot > tr > td {
+  border-top: 1px solid var(--color-secondary);
+  background: var(--color-box-body);
+  text-align: inherit;
+  color: var(--color-text);
+  padding: 0.78571429em;
+  vertical-align: inherit;
+  font-weight: var(--font-weight-normal);
+}
+.ui.table > tfoot > tr > th:first-child,
+.ui.table > tfoot > tr > td:first-child {
+  border-left: none;
+}
+.ui.table > tfoot > tr:first-child > th:first-child,
+.ui.table > tfoot > tr:first-child > td:first-child {
+  border-radius: 0 0 0 0.28571429rem;
+}
+.ui.table > tfoot > tr:first-child > th:last-child,
+.ui.table > tfoot > tr:first-child > td:last-child {
+  border-radius: 0 0 0.28571429rem;
+}
+.ui.table > tfoot > tr:first-child > th:only-child,
+.ui.table > tfoot > tr:first-child > td:only-child {
+  border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+.ui.table > tr > td,
+.ui.table > tbody > tr > td {
+  border-top: 1px solid var(--color-secondary-alpha-50);
+  padding: 6px 5px;
+  text-align: inherit;
+}
+.ui.table > tr:first-child > td,
+.ui.table > tbody > tr:first-child > td {
+  border-top: none;
+}
+
+.ui.table.segment {
+  padding: 0;
+}
+.ui.table.segment::after {
+  display: none;
+}
+
+@media only screen and (max-width: 767.98px) {
+  .ui.table:not(.unstackable) {
+    width: 100%;
+    padding: 0;
+  }
+  .ui.table:not(.unstackable) > thead,
+  .ui.table:not(.unstackable) > thead > tr,
+  .ui.table:not(.unstackable) > tfoot,
+  .ui.table:not(.unstackable) > tfoot > tr,
+  .ui.table:not(.unstackable) > tbody,
+  .ui.table:not(.unstackable) > tr,
+  .ui.table:not(.unstackable) > tbody > tr,
+  .ui.table:not(.unstackable) > tr > th,
+  .ui.table:not(.unstackable) > thead > tr > th,
+  .ui.table:not(.unstackable) > tbody > tr > th,
+  .ui.table:not(.unstackable) > tfoot > tr > th,
+  .ui.table:not(.unstackable) > tr > td,
+  .ui.table:not(.unstackable) > tbody > tr > td,
+  .ui.table:not(.unstackable) > tfoot > tr > td {
+    display: block !important;
+    width: auto !important;
+  }
+  .ui.table:not(.unstackable) > thead {
+    display: block;
+  }
+  .ui.table:not(.unstackable) > tfoot {
+    display: block;
+  }
+  .ui.ui.ui.ui.table:not(.unstackable) > tr,
+  .ui.ui.ui.ui.table:not(.unstackable) > thead > tr,
+  .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr,
+  .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr {
+    padding-top: 1em;
+    padding-bottom: 1em;
+  }
+  .ui.ui.ui.ui.table:not(.unstackable) > tr > th,
+  .ui.ui.ui.ui.table:not(.unstackable) > thead > tr > th,
+  .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > th,
+  .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > th,
+  .ui.ui.ui.ui.table:not(.unstackable) > tr > td,
+  .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > td,
+  .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > td {
+    background: none;
+    border: none;
+    padding: 0.25em 0.75em;
+  }
+  .ui.table:not(.unstackable) > tr > th:first-child,
+  .ui.table:not(.unstackable) > thead > tr > th:first-child,
+  .ui.table:not(.unstackable) > tbody > tr > th:first-child,
+  .ui.table:not(.unstackable) > tfoot > tr > th:first-child,
+  .ui.table:not(.unstackable) > tr > td:first-child,
+  .ui.table:not(.unstackable) > tbody > tr > td:first-child,
+  .ui.table:not(.unstackable) > tfoot > tr > td:first-child {
+    font-weight: var(--font-weight-normal);
+  }
+}
+
+.ui.table[class*="left aligned"],
+.ui.table [class*="left aligned"] {
+  text-align: left;
+}
+
+.ui.table[class*="center aligned"],
+.ui.table [class*="center aligned"] {
+  text-align: center;
+}
+
+.ui.table[class*="right aligned"],
+.ui.table [class*="right aligned"] {
+  text-align: right;
+}
+
+.ui.table[class*="top aligned"],
+.ui.table [class*="top aligned"] {
+  vertical-align: top;
+}
+
+.ui.table[class*="middle aligned"],
+.ui.table [class*="middle aligned"] {
+  vertical-align: middle;
+}
+
+.ui.table th.collapsing,
+.ui.table td.collapsing {
+  width: 1px;
+  white-space: nowrap;
+}
+
+.ui.fixed.table {
+  table-layout: fixed;
+}
+.ui.fixed.table th,
+.ui.fixed.table td {
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.ui.attached.table {
+  top: 0;
+  bottom: 0;
+  border-radius: 0;
+  margin: 0 -1px;
+  width: calc(100% + 2px);
+  max-width: calc(100% + 2px);
+  border: 1px solid var(--color-secondary);
+}
+.ui.attached + .ui.attached.table:not(.top) {
+  border-top: none;
+}
+
+.ui[class*="bottom attached"].table {
+  bottom: 0;
+  margin-top: 0;
+  top: 0;
+  margin-bottom: 1em;
+  border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+.ui[class*="bottom attached"].table:last-child {
+  margin-bottom: 0;
+}
+
+.ui.striped.table > tr:nth-child(2n),
+.ui.striped.table > tbody > tr:nth-child(2n) {
+  background: var(--color-light);
+}
+
+.ui.table[class*="single line"],
+.ui.table [class*="single line"] {
+  white-space: nowrap;
+}
+
+/* Column Width */
+.ui.table th.one.wide,
+.ui.table td.one.wide {
+  width: 6.25%;
+}
+.ui.table th.two.wide,
+.ui.table td.two.wide {
+  width: 12.5%;
+}
+.ui.table th.three.wide,
+.ui.table td.three.wide {
+  width: 18.75%;
+}
+.ui.table th.four.wide,
+.ui.table td.four.wide {
+  width: 25%;
+}
+.ui.table th.five.wide,
+.ui.table td.five.wide {
+  width: 31.25%;
+}
+.ui.table th.six.wide,
+.ui.table td.six.wide {
+  width: 37.5%;
+}
+.ui.table th.seven.wide,
+.ui.table td.seven.wide {
+  width: 43.75%;
+}
+.ui.table th.eight.wide,
+.ui.table td.eight.wide {
+  width: 50%;
+}
+.ui.table th.nine.wide,
+.ui.table td.nine.wide {
+  width: 56.25%;
+}
+.ui.table th.ten.wide,
+.ui.table td.ten.wide {
+  width: 62.5%;
+}
+.ui.table th.eleven.wide,
+.ui.table td.eleven.wide {
+  width: 68.75%;
+}
+.ui.table th.twelve.wide,
+.ui.table td.twelve.wide {
+  width: 75%;
+}
+.ui.table th.thirteen.wide,
+.ui.table td.thirteen.wide {
+  width: 81.25%;
+}
+.ui.table th.fourteen.wide,
+.ui.table td.fourteen.wide {
+  width: 87.5%;
+}
+.ui.table th.fifteen.wide,
+.ui.table td.fifteen.wide {
+  width: 93.75%;
+}
+.ui.table th.sixteen.wide,
+.ui.table td.sixteen.wide {
+  width: 100%;
+}
+
+.ui.basic.table {
+  background: transparent;
+  border: 1px solid var(--color-secondary);
+}
+.ui.basic.table > thead > tr > th,
+.ui.basic.table > tbody > tr > th,
+.ui.basic.table > tfoot > tr > th,
+.ui.basic.table > tr > th {
+  background: transparent;
+  border-left: none;
+}
+.ui.basic.table > tbody > tr {
+  border-bottom: 1px solid var(--color-secondary);
+}
+.ui.basic.table > tbody > tr > td,
+.ui.basic.table > tfoot > tr > td,
+.ui.basic.table > tr > td {
+  background: transparent;
+}
+.ui.basic.striped.table > tbody > tr:nth-child(2n) {
+  background: var(--color-light);
+}
+
+.ui[class*="very basic"].table {
+  border: none;
+}
+.ui[class*="very basic"].table:not(.striped) > tr > th:first-child,
+.ui[class*="very basic"].table:not(.striped) > thead > tr > th:first-child,
+.ui[class*="very basic"].table:not(.striped) > tbody > tr > th:first-child,
+.ui[class*="very basic"].table:not(.striped) > tfoot > tr > th:first-child,
+.ui[class*="very basic"].table:not(.striped) > tr > td:first-child,
+.ui[class*="very basic"].table:not(.striped) > tbody > tr > td:first-child,
+.ui[class*="very basic"].table:not(.striped) > tfoot > tr > td:first-child {
+  padding-left: 0;
+}
+.ui[class*="very basic"].table:not(.striped) > tr > th:last-child,
+.ui[class*="very basic"].table:not(.striped) > thead > tr > th:last-child,
+.ui[class*="very basic"].table:not(.striped) > tbody > tr > th:last-child,
+.ui[class*="very basic"].table:not(.striped) > tfoot > tr > th:last-child,
+.ui[class*="very basic"].table:not(.striped) > tr > td:last-child,
+.ui[class*="very basic"].table:not(.striped) > tbody > tr > td:last-child,
+.ui[class*="very basic"].table:not(.striped) > tfoot > tr > td:last-child {
+  padding-right: 0;
+}
+.ui[class*="very basic"].table:not(.striped) > thead > tr:first-child > th {
+  padding-top: 0;
+}
+
+.ui.celled.table > tr > th,
+.ui.celled.table > thead > tr > th,
+.ui.celled.table > tbody > tr > th,
+.ui.celled.table > tfoot > tr > th,
+.ui.celled.table > tr > td,
+.ui.celled.table > tbody > tr > td,
+.ui.celled.table > tfoot > tr > td {
+  border-left: 1px solid var(--color-secondary-alpha-50);
+}
+.ui.celled.table > tr > th:first-child,
+.ui.celled.table > thead > tr > th:first-child,
+.ui.celled.table > tbody > tr > th:first-child,
+.ui.celled.table > tfoot > tr > th:first-child,
+.ui.celled.table > tr > td:first-child,
+.ui.celled.table > tbody > tr > td:first-child,
+.ui.celled.table > tfoot > tr > td:first-child {
+  border-left: none;
+}
+
+.ui.compact.table > tr > th,
+.ui.compact.table > thead > tr > th,
+.ui.compact.table > tbody > tr > th,
+.ui.compact.table > tfoot > tr > th {
+  padding-left: 0.7em;
+  padding-right: 0.7em;
+}
+.ui.compact.table > tr > td,
+.ui.compact.table > tbody > tr > td,
+.ui.compact.table > tfoot > tr > td {
+  padding: 0.5em 0.7em;
+}
+
+/* use more horizontal padding on first and last items for visuals */
+.ui.table > thead > tr > th:first-of-type,
+.ui.table > tbody > tr > td:first-of-type,
+.ui.table > tr > td:first-of-type {
+  padding-left: 10px;
+}
+.ui.table > thead > tr > th:last-of-type,
+.ui.table > tbody > tr > td:last-of-type,
+.ui.table > tr > td:last-of-type {
+  padding-right: 10px;
+}
diff --git a/web_src/css/modules/tippy.css b/web_src/css/modules/tippy.css
index d65ecc89fb..6ac7c37d93 100644
--- a/web_src/css/modules/tippy.css
+++ b/web_src/css/modules/tippy.css
@@ -5,6 +5,11 @@
   display: none !important;
 }
 
+/* show target element once it's been moved by tippy.js */
+.tippy-content .tippy-target {
+  display: unset !important;
+}
+
 [data-tippy-root] {
   max-width: calc(100vw - 32px);
 }
@@ -24,6 +29,17 @@
   z-index: 1;
 }
 
+/* bare theme, no styling at all, except box-shadow */
+.tippy-box[data-theme="bare"] {
+  border: none;
+  box-shadow: 0 6px 18px var(--color-shadow);
+}
+
+.tippy-box[data-theme="bare"] .tippy-content {
+  padding: 0;
+  background: transparent;
+}
+
 /* tooltip theme for text tooltips */
 
 .tippy-box[data-theme="tooltip"] {
@@ -46,18 +62,40 @@
 .tippy-box[data-theme="menu"] {
   background-color: var(--color-menu);
   color: var(--color-text);
+  box-shadow: 0 6px 18px var(--color-shadow);
 }
 
 .tippy-box[data-theme="menu"] .tippy-content {
-  padding: 0;
+  padding: 4px 0;
 }
 
 .tippy-box[data-theme="menu"] .tippy-svg-arrow-inner {
   fill: var(--color-menu);
 }
 
+.tippy-box[data-theme="menu"] .item {
+  display: flex;
+  align-items: center;
+  padding: 9px 18px;
+  color: inherit;
+  text-decoration: none;
+  gap: 10px;
+}
+
+.tippy-box[data-theme="menu"] .item:hover {
+  background: var(--color-hover);
+}
+
+.tippy-box[data-theme="menu"] .item:focus {
+  background: var(--color-active);
+}
+
 /* box-with-header theme to look like .ui.attached.segment. can contain .ui.attached.header */
 
+.tippy-box[data-theme="box-with-header"] {
+  box-shadow: 0 6px 18px var(--color-shadow);
+}
+
 .tippy-box[data-theme="box-with-header"] .tippy-content {
   background: var(--color-box-body);
   border-radius: var(--border-radius);
diff --git a/web_src/css/org.css b/web_src/css/org.css
index d2bf0ff606..32e8a914fa 100644
--- a/web_src/css/org.css
+++ b/web_src/css/org.css
@@ -89,50 +89,44 @@
   text-align: center;
 }
 
-.organization.options input {
-  min-width: 300px;
-}
-
-.organization.profile .org-avatar {
-  width: 100px;
-  height: 100px;
+.page-content.organization .org-avatar {
   margin-right: 15px;
 }
 
-.organization.profile #org-info {
+.page-content.organization #org-info {
   overflow-wrap: anywhere;
   flex: 1;
   word-break: break-all;
 }
 
-.organization.profile #org-info .ui.header {
+.page-content.organization #org-info .ui.header {
   display: flex;
   align-items: center;
   font-size: 36px;
   margin-bottom: 0;
 }
 
-.organization.profile #org-info .desc {
+.page-content.organization #org-info .desc {
   font-size: 16px;
   margin-bottom: 10px;
 }
 
-.organization.profile #org-info .meta {
+.page-content.organization #org-info .meta {
   display: flex;
   align-items: center;
   flex-wrap: wrap;
   gap: 8px;
 }
 
-.organization.profile .ui.top.header .ui.right {
+.page-content.organization .ui.top.header .ui.right {
   margin-top: 0;
 }
 
-.organization.profile .teams .item {
+.page-content.organization .teams .item {
   padding: 10px 15px;
 }
 
-.organization.profile .members .ui.avatar {
+.page-content.organization .members .ui.avatar {
   width: 48px;
   height: 48px;
   margin-right: 5px;
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 55c6ec4817..c579745238 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -4,16 +4,6 @@
   user-select: none;
 }
 
-.repository .navbar {
-  display: flex;
-  justify-content: space-between;
-}
-
-.repository .navbar .ui.label {
-  margin-left: 7px;
-  padding: 3px 5px;
-}
-
 .repository .owner.dropdown {
   min-width: 40% !important;
 }
@@ -143,41 +133,31 @@
   margin-bottom: 12px;
 }
 
-.repository #clone-panel #repo-clone-url {
+.repository .clone-panel #repo-clone-url {
   width: 320px;
   border-radius: 0;
 }
 
-@media (min-width: 768px) and (max-width: 991.98px) {
-  .repository #clone-panel #repo-clone-url {
+@media (max-width: 991.98px) {
+  .repository .clone-panel #repo-clone-url {
     width: 200px;
   }
 }
 
-@media (max-width: 767.98px) {
-  .repository #clone-panel #repo-clone-url {
-    width: 200px;
-  }
+.repository .ui.action.input.clone-panel > button + button,
+.repository .ui.action.input.clone-panel > button + input {
+  margin-left: -1px; /* make the borders overlap to avoid double borders */
 }
 
-.repository #clone-panel #repo-clone-https,
-.repository #clone-panel #repo-clone-ssh {
-  border-right: none;
-}
-
-.repository #clone-panel #more-btn {
-  border-left: none;
-}
-
-.repository #clone-panel button:first-of-type {
+.repository .clone-panel > button:first-of-type {
   border-radius: var(--border-radius) 0 0 var(--border-radius) !important;
 }
 
-.repository #clone-panel button:last-of-type {
+.repository .clone-panel > button:last-of-type {
   border-radius: 0 var(--border-radius) var(--border-radius) 0 !important;
 }
 
-.repository #clone-panel .dropdown .menu {
+.repository .clone-panel .dropdown .menu {
   right: 0 !important;
   left: auto !important;
 }
@@ -197,12 +177,44 @@
   }
 }
 
-.repository.file.list .repo-path {
-  word-break: break-word;
+.commit-summary {
+  flex: 1;
+  overflow-wrap: anywhere;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
 }
 
-.repository.file.list #repo-files-table {
-  table-layout: fixed;
+.commit-header .commit-summary,
+td .commit-summary {
+  white-space: normal;
+}
+
+.latest-commit {
+  display: flex;
+  flex: 1;
+  align-items: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+@media (max-width: 767.98px) {
+  .latest-commit .sha {
+    display: none;
+  }
+  .latest-commit .commit-summary {
+    margin-left: 8px;
+  }
+}
+
+.repo-path {
+  display: flex;
+  overflow-wrap: anywhere;
+}
+
+/* this is what limits the commit table width to a value that works on all viewport sizes */
+#repo-files-table th:first-of-type {
+  max-width: calc(calc(min(100vw, 1280px)) - 145px - calc(2 * var(--page-margin-x)));
 }
 
 .repository.file.list #repo-files-table thead th {
@@ -282,7 +294,6 @@
 }
 
 .repository.file.list #repo-files-table td.age {
-  width: 120px;
   color: var(--color-text-light-1);
 }
 
@@ -935,18 +946,11 @@
 
 .repository.view.issue .comment-list .comment .merge-section .item-section {
   display: flex;
+  flex-wrap: wrap;
   align-items: center;
   justify-content: space-between;
   padding: 0;
-  margin-top: -0.25rem;
-  margin-bottom: -0.25rem;
-}
-
-@media (max-width: 767.98px) {
-  .repository.view.issue .comment-list .comment .merge-section .item-section {
-    align-items: flex-start;
-    flex-direction: column;
-  }
+  gap: 0.5em;
 }
 
 .repository.view.issue .comment-list .comment .merge-section .divider {
@@ -1072,11 +1076,7 @@
 
 .repository.view.issue .comment-list .event .detail {
   margin-top: 4px;
-  margin-left: 14px;
-}
-
-.repository.view.issue .comment-list .event .detail .svg {
-  margin-right: 2px;
+  margin-left: 15px;
 }
 
 .repository.view.issue .comment-list .event .segments {
@@ -1250,10 +1250,6 @@
   margin: 0;
 }
 
-.repository #commits-table td.message {
-  text-overflow: unset;
-}
-
 .repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) {
   background-color: var(--color-light) !important;
 }
@@ -1427,6 +1423,7 @@
 
 .repository .data-table tr {
   border-top: 0;
+  background: none !important;
 }
 
 .repository .data-table td,
@@ -1439,6 +1436,21 @@
   border: 1px solid var(--color-secondary);
 }
 
+/* the border css competes with .markup where all tables have outer border which would add a double
+   border here, remove only the outer borders from this table */
+.repository .data-table tr:first-child :is(td,th) {
+  border-top: none !important;
+}
+.repository .data-table tr:last-child :is(td,th) {
+  border-bottom: none !important;
+}
+.repository .data-table tr :is(td,th):first-child {
+  border-left: none !important;
+}
+.repository .data-table tr :is(td,th):last-child {
+  border-right: none !important;
+}
+
 .repository .data-table td {
   white-space: pre-line;
 }
@@ -1476,7 +1488,7 @@
   min-width: 50px;
   font-family: monospace;
   line-height: 20px;
-  color: var(--color-secondary-dark-2);
+  color: var(--color-text-light-1);
   white-space: nowrap;
   vertical-align: top;
   cursor: pointer;
@@ -1498,12 +1510,6 @@
   background: var(--color-body);
 }
 
-@media (max-width: 991.98px) {
-  .repository .diff-detail-box {
-    flex-direction: row;
-  }
-}
-
 @media (max-width: 480px) {
   .repository .diff-detail-box {
     flex-wrap: wrap;
@@ -1528,7 +1534,7 @@
   color: var(--color-red);
 }
 
-@media (max-width: 991.98px) {
+@media (max-width: 800px) {
   .repository .diff-detail-box .diff-detail-stats {
     display: none !important;
   }
@@ -1538,7 +1544,6 @@
   display: flex;
   align-items: center;
   gap: 0.25em;
-  flex-wrap: wrap;
   justify-content: end;
 }
 
@@ -1548,15 +1553,6 @@
   margin-right: 0 !important;
 }
 
-@media (max-width: 480px) {
-  .repository .diff-detail-box .diff-detail-actions {
-    padding-top: 0.25rem;
-  }
-  .repository .diff-detail-box .diff-detail-actions .ui.button:not(.btn-submit) {
-    padding: 0 0.5rem;
-  }
-}
-
 .repository .diff-detail-box span.status {
   display: inline-block;
   width: 12px;
@@ -1633,7 +1629,6 @@
 
 .repository .diff-file-box .file-body.file-code .lines-num {
   text-align: right;
-  color: var(--color-text-light);
   width: 1%;
   min-width: 50px;
 }
@@ -1770,10 +1765,6 @@
   font-weight: var(--font-weight-normal);
 }
 
-.repository.quickstart .guide .clone.button:first-child {
-  border-radius: var(--border-radius) 0 0 var(--border-radius);
-}
-
 .repository.quickstart .guide #repo-clone-url {
   border-radius: 0;
   padding: 5px 10px;
@@ -2046,13 +2037,8 @@
 }
 
 #cite-repo-modal #citation-panel {
-  width: 500px;
-}
-
-@media (max-width: 767.98px) {
-  #cite-repo-modal #citation-panel {
-    width: 100%;
-  }
+  display: flex;
+  width: 100%;
 }
 
 #cite-repo-modal #citation-panel input {
@@ -2072,6 +2058,7 @@
   padding: 5px 10px;
   font-size: 1.2em;
   line-height: 1.4;
+  flex: 1;
 }
 
 #cite-repo-modal #citation-panel #citation-copy-apa,
@@ -2154,6 +2141,20 @@
   padding-bottom: 0 !important;
 }
 
+.commit-header-buttons {
+  display: flex;
+  gap: 4px;
+  align-items: flex-start;
+  white-space: nowrap;
+}
+
+@media (max-width: 767.98px) {
+  .commit-header-buttons {
+    flex-direction: column;
+    align-items: stretch;
+  }
+}
+
 .settings.webhooks .list > .item:not(:first-child),
 .settings.githooks .list > .item:not(:first-child),
 .settings.actions .list > .item:not(:first-child) {
@@ -2289,10 +2290,6 @@
   padding: 1em;
 }
 
-.comment-body .markup {
-  border-radius: 0 0 var(--border-radius) var(--border-radius); /* don't render outside box */
-}
-
 .edit-label.modal .form .column,
 .new-label.modal .form .column {
   padding-right: 0;
@@ -2304,104 +2301,6 @@
   padding-top: 15px;
 }
 
-.edit-label.modal .form .color.picker.column,
-.new-label.modal .form .color.picker.column {
-  display: flex;
-}
-
-.edit-label.modal .form .color.picker.column .minicolors,
-.new-label.modal .form .color.picker.column .minicolors {
-  flex: 1;
-}
-
-.edit-label.modal .form .minicolors-swatch.minicolors-sprite,
-.new-label.modal .form .minicolors-swatch.minicolors-sprite {
-  top: 10px;
-  left: 10px;
-  width: 15px;
-  height: 15px;
-}
-
-.tab-size-1 {
-  tab-size: 1 !important;
-  -moz-tab-size: 1 !important;
-}
-
-.tab-size-2 {
-  tab-size: 2 !important;
-  -moz-tab-size: 2 !important;
-}
-
-.tab-size-3 {
-  tab-size: 3 !important;
-  -moz-tab-size: 3 !important;
-}
-
-.tab-size-4 {
-  tab-size: 4 !important;
-  -moz-tab-size: 4 !important;
-}
-
-.tab-size-5 {
-  tab-size: 5 !important;
-  -moz-tab-size: 5 !important;
-}
-
-.tab-size-6 {
-  tab-size: 6 !important;
-  -moz-tab-size: 6 !important;
-}
-
-.tab-size-7 {
-  tab-size: 7 !important;
-  -moz-tab-size: 7 !important;
-}
-
-.tab-size-8 {
-  tab-size: 8 !important;
-  -moz-tab-size: 8 !important;
-}
-
-.tab-size-9 {
-  tab-size: 9 !important;
-  -moz-tab-size: 9 !important;
-}
-
-.tab-size-10 {
-  tab-size: 10 !important;
-  -moz-tab-size: 10 !important;
-}
-
-.tab-size-11 {
-  tab-size: 11 !important;
-  -moz-tab-size: 11 !important;
-}
-
-.tab-size-12 {
-  tab-size: 12 !important;
-  -moz-tab-size: 12 !important;
-}
-
-.tab-size-13 {
-  tab-size: 13 !important;
-  -moz-tab-size: 13 !important;
-}
-
-.tab-size-14 {
-  tab-size: 14 !important;
-  -moz-tab-size: 14 !important;
-}
-
-.tab-size-15 {
-  tab-size: 15 !important;
-  -moz-tab-size: 15 !important;
-}
-
-.tab-size-16 {
-  tab-size: 16 !important;
-  -moz-tab-size: 16 !important;
-}
-
 .stats-table {
   display: table;
   width: 100%;
@@ -2415,8 +2314,21 @@
   height: 0.5em;
 }
 
+.labels-list {
+  display: inline-flex;
+  flex-wrap: wrap;
+  gap: 2.5px;
+}
+
+.labels-list a {
+  display: flex;
+  text-decoration: none;
+}
+
 .labels-list .label {
-  margin: 2px 0;
+  padding: 0 6px;
+  margin: 0 !important;
+  min-height: 20px;
   display: inline-flex !important;
   line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */
 }
@@ -2428,6 +2340,11 @@
   gap: 0 !important;
 }
 
+.archived-label {
+  filter: grayscale(0.5);
+  opacity: 0.5;
+}
+
 .ui.label.scope-left {
   border-bottom-right-radius: 0;
   border-top-right-radius: 0;
@@ -2470,13 +2387,14 @@ tbody.commit-list {
 .author-wrapper {
   overflow: hidden;
   text-overflow: ellipsis;
-  max-width: calc(100% - 50px);
+  max-width: 100%;
   display: inline-block;
   vertical-align: middle;
 }
 
 .author-wrapper {
   max-width: 180px;
+  align-self: center;
 }
 
 /* in the commit list, messages can wrap so we can use inline */
@@ -2494,10 +2412,6 @@ tbody.commit-list {
   tr.commit-list {
     width: 100%;
   }
-  th .message-wrapper {
-    display: block;
-    max-width: calc(100vw - 70px);
-  }
   .author-wrapper {
     max-width: 80px;
   }
@@ -2507,27 +2421,18 @@ tbody.commit-list {
   tr.commit-list {
     width: 723px;
   }
-  th .message-wrapper {
-    max-width: 120px;
-  }
 }
 
 @media (min-width: 992px) and (max-width: 1200px) {
   tr.commit-list {
     width: 933px;
   }
-  th .message-wrapper {
-    max-width: 350px;
-  }
 }
 
 @media (min-width: 1201px) {
   tr.commit-list {
     width: 1127px;
   }
-  th .message-wrapper {
-    max-width: 525px;
-  }
 }
 
 .commit-list .commit-status-link {
@@ -2573,6 +2478,7 @@ tbody.commit-list {
 #repo-topics .repo-topic {
   font-weight: var(--font-weight-normal);
   cursor: pointer;
+  margin: 0;
 }
 
 #new-dependency-drop-list.ui.selection.dropdown {
@@ -2798,16 +2704,6 @@ tbody.commit-list {
   border-left: 1px solid var(--color-secondary);
 }
 
-.repository .ui.menu.new-menu {
-  background: none !important;
-}
-
-@media (max-width: 1200px) {
-  .repository .ui.menu.new-menu::after {
-    background: none !important;
-  }
-}
-
 .migrate-entries {
   display: grid !important;
   grid-template-columns: repeat(3, 1fr);
@@ -2864,7 +2760,7 @@ tbody.commit-list {
   .repository.file.list #repo-files-table .entry td.message,
   .repository.file.list #repo-files-table .commit-list td.message,
   .repository.file.list #repo-files-table .entry span.commit-summary,
-  .repository.file.list #repo-files-table .commit-list span.commit-summary {
+  .repository.file.list #repo-files-table .commit-list tr span.commit-summary {
     display: none !important;
   }
   .repository.view.issue .comment-list .timeline,
@@ -3000,6 +2896,7 @@ tbody.commit-list {
   display: flex;
   align-items: center;
   justify-content: flex-end;
+  gap: 8px;
 }
 
 @media (max-width: 767.98px) {
@@ -3034,3 +2931,33 @@ tbody.commit-list {
 #cherry-pick-modal .scrolling.menu {
   max-height: 200px;
 }
+
+/* Branch tag selector - TODO: Merge this into the same selector on repo page */
+.repository .issue-content .issue-content-right  .ui.grid .column.row {
+  padding: 10px;
+  padding-bottom: 0;
+}
+.repository .issue-content .issue-content-right  .ui.grid .column.muted {
+  padding: 0;
+}
+.repository .issue-content .issue-content-right  .ui.grid .column.muted .text {
+  display: inline-block;
+  padding: 10px;
+  width: 100%;
+  text-align: center;
+  border: 1px solid transparent;
+  border-bottom: none;
+}
+.repository .issue-content .issue-content-right .ui.grid .column.muted .text.black {
+  border-color: var(--color-secondary);
+  background: var(--color-menu);
+  border-top-left-radius: var(--border-radius);
+  border-top-right-radius: var(--border-radius);
+}
+.repository .issue-content .issue-content-right .ui.dropdown  .scrolling.menu {
+  border-top: none;
+}
+.repository .issue-content .issue-content-right .branch-tag-divider {
+  margin-top: -1px;
+  border-top: 1px solid var(--color-secondary);
+}
diff --git a/web_src/css/repo/header.css b/web_src/css/repo/header.css
index d5c7d212e8..55e704ed10 100644
--- a/web_src/css/repo/header.css
+++ b/web_src/css/repo/header.css
@@ -1,4 +1,8 @@
-.fork-flag {
+.repository .secondary-nav {
+  padding-top: 12px;
+}
+
+.repository .secondary-nav .fork-flag {
   margin-top: 0.5rem;
   font-size: 12px;
 }
@@ -8,17 +12,15 @@
   flex-flow: row wrap;
   justify-content: space-between;
   gap: 0.5rem;
+  margin-bottom: 4px;
 }
 
 .repo-header .flex-item {
   padding: 0;
 }
 
-.repo-header .btn.interact-bg:hover {
-  text-decoration: none;
-}
-
 .repo-header .flex-item-main {
+  flex: 0;
   flex-basis: unset;
 }
 
@@ -26,10 +28,6 @@
   flex-wrap: nowrap;
 }
 
-.repo-header .flex-item-trailing .repo-icon {
-  display: none;
-}
-
 .repo-buttons {
   align-items: center;
   display: flex;
@@ -69,33 +67,3 @@
 .repo-buttons .ui.labeled.button.disabled > .button {
   pointer-events: none !important;
 }
-
-.repository .header-wrapper {
-  background-color: var(--color-header-wrapper);
-}
-
-.repository .header-wrapper .new-menu {
-  padding-top: 0 !important;
-  margin-top: 0 !important;
-  margin-bottom: 0 !important;
-}
-
-.repository .header-wrapper .new-menu .item {
-  margin-left: 0 !important;
-  margin-right: 0 !important;
-}
-
-@media (max-width: 767.98px) {
-  .repo-header .flex-item {
-    flex-grow: 1;
-  }
-  .repo-buttons .ui.labeled.button .text {
-    display: none;
-  }
-  .repo-header .flex-item-trailing .label {
-    display: none;
-  }
-  .repo-header .flex-item-trailing .repo-icon {
-    display: initial;
-  }
-}
diff --git a/web_src/css/repo/issue-card.css b/web_src/css/repo/issue-card.css
index 5a70de47c2..b9368df4f6 100644
--- a/web_src/css/repo/issue-card.css
+++ b/web_src/css/repo/issue-card.css
@@ -19,3 +19,7 @@
   font-size: 14px;
   margin-left: 4px;
 }
+
+.issue-card.sortable-chosen .issue-card-title {
+  cursor: inherit;
+}
diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css
index 1421577af2..77905956f0 100644
--- a/web_src/css/repo/issue-list.css
+++ b/web_src/css/repo/issue-list.css
@@ -9,6 +9,7 @@
 
 .issue-list-toolbar-left {
   display: flex;
+  align-items: center;
 }
 
 .issue-list-toolbar-right .filter.menu {
@@ -33,23 +34,6 @@
   }
 }
 
-#issue-list .flex-item-title .labels-list {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 0.25em;
-}
-
-#issue-list .flex-item-title .labels-list a {
-  display: flex;
-  text-decoration: none;
-}
-
-#issue-list .flex-item-title .labels-list .label {
-  padding: 0 6px;
-  margin: 0;
-  min-height: 20px;
-}
-
 #issue-list .flex-item-body .branches {
   display: inline-flex;
 }
diff --git a/web_src/css/repo/linebutton.css b/web_src/css/repo/linebutton.css
index 1e5e51eac5..e99d0399d1 100644
--- a/web_src/css/repo/linebutton.css
+++ b/web_src/css/repo/linebutton.css
@@ -2,24 +2,17 @@
   color: var(--color-text-dark) !important;
 }
 
-.code-line-menu {
-  width: auto !important;
-  border: none !important; /* the border is provided by tippy, not using the `.ui.menu` border */
-}
-
 .code-line-button {
-  background-color: var(--color-menu);
-  color: var(--color-text-light);
   border: 1px solid var(--color-secondary);
   border-radius: var(--border-radius);
-  padding: 1px 10px;
+  padding: 1px 4px !important;
   position: absolute;
   font-family: var(--fonts-regular);
   left: 0;
-  transform: translateX(-50%);
+  transform: translateX(calc(-50% + 6px));
   cursor: pointer;
 }
 
 .code-line-button:hover {
-  color: var(--color-primary);
+  background: var(--color-secondary) !important;
 }
diff --git a/web_src/css/repo/wiki.css b/web_src/css/repo/wiki.css
index 1302e9cb5c..ba502d3216 100644
--- a/web_src/css/repo/wiki.css
+++ b/web_src/css/repo/wiki.css
@@ -21,6 +21,7 @@
 
 .repository.wiki .wiki-content-parts .markup {
   border: 1px solid var(--color-secondary);
+  border-radius: var(--border-radius);
   padding: 1em;
   margin-top: 1em;
   font-size: 1em;
@@ -58,7 +59,7 @@
 }
 
 @media (max-width: 767.98px) {
-  .repository.wiki #clone-panel #repo-clone-url {
+  .repository.wiki .clone-panel #repo-clone-url {
     width: 160px;
   }
   .repository.wiki .wiki-content-main.with-sidebar,
diff --git a/web_src/css/review.css b/web_src/css/review.css
index 5336775547..7534500e6f 100644
--- a/web_src/css/review.css
+++ b/web_src/css/review.css
@@ -96,9 +96,6 @@
 }
 
 @media (max-width: 767.98px) {
-  .comment-code-cloud .comments .comment {
-    display: flex;
-  }
   .comment-code-cloud .comments .comment .comment-header-right.actions .ui.basic.label {
     display: none;
   }
@@ -194,6 +191,10 @@
   margin: 0 0 0 3em;
 }
 
+.diff-file-body.binary {
+  padding: 5px 10px;
+}
+
 .file-comment {
   color: var(--color-text);
 }
diff --git a/web_src/css/shared/flex-list.css b/web_src/css/shared/flex-list.css
index e1ad6e7f97..6217b45300 100644
--- a/web_src/css/shared/flex-list.css
+++ b/web_src/css/shared/flex-list.css
@@ -86,7 +86,7 @@
   border-top: 1px solid var(--color-secondary);
 }
 
-/* Fomantic UI segment has default "padding: 1em", so here it removes the padding-top and padding-bottom accordingly (there might also be some `gt-hidden` siblings).
+/* Fomantic UI segment has default "padding: 1em", so here it removes the padding-top and padding-bottom accordingly (there might also be some `tw-hidden` siblings).
 Developers could also use "flex-space-fitted" class to remove the first item's padding-top and the last item's padding-bottom */
 .flex-list.flex-space-fitted > .flex-item:first-child,
 .ui.segment > .flex-list > .flex-item:first-child {
diff --git a/web_src/css/shared/repoorg.css b/web_src/css/shared/repoorg.css
index 7f0a805d0f..5573ae47b8 100644
--- a/web_src/css/shared/repoorg.css
+++ b/web_src/css/shared/repoorg.css
@@ -5,13 +5,6 @@
   margin-left: 15px;
 }
 
-.repository .ui.secondary.stackable.pointing.menu,
-.organization .ui.secondary.stackable.pointing.menu {
-  flex-wrap: wrap;
-  margin-top: 5px;
-  margin-bottom: 10px;
-}
-
 .repository .ui.tabs.container,
 .organization .ui.tabs.container {
   margin-top: 14px;
diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index bac002e3db..7bf2c982c6 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -3,72 +3,72 @@
 
 :root {
   --is-dark-theme: true;
-  --color-primary: #87ab63;
+  --color-primary: #4183c4;
   --color-primary-contrast: #ffffff;
-  --color-primary-dark-1: #93b373;
-  --color-primary-dark-2: #9fbc82;
-  --color-primary-dark-3: #abc492;
-  --color-primary-dark-4: #b7cda1;
-  --color-primary-dark-5: #cfddc1;
-  --color-primary-dark-6: #e7eee0;
-  --color-primary-dark-7: #f8faf6;
-  --color-primary-light-1: #7a9e55;
-  --color-primary-light-2: #6c8c4c;
-  --color-primary-light-3: #5f7b42;
-  --color-primary-light-4: #516939;
-  --color-primary-light-5: #364626;
-  --color-primary-light-6: #1b2313;
-  --color-primary-light-7: #080b06;
-  --color-primary-alpha-10: #87ab6319;
-  --color-primary-alpha-20: #87ab6333;
-  --color-primary-alpha-30: #87ab634b;
-  --color-primary-alpha-40: #87ab6366;
-  --color-primary-alpha-50: #87ab6380;
-  --color-primary-alpha-60: #87ab6399;
-  --color-primary-alpha-70: #87ab63b3;
-  --color-primary-alpha-80: #87ab63cc;
-  --color-primary-alpha-90: #87ab63e1;
+  --color-primary-dark-1: #548fca;
+  --color-primary-dark-2: #679cd0;
+  --color-primary-dark-3: #7aa8d6;
+  --color-primary-dark-4: #8db5dc;
+  --color-primary-dark-5: #b3cde7;
+  --color-primary-dark-6: #d9e6f3;
+  --color-primary-dark-7: #f4f8fb;
+  --color-primary-light-1: #3876b3;
+  --color-primary-light-2: #31699f;
+  --color-primary-light-3: #2b5c8b;
+  --color-primary-light-4: #254f77;
+  --color-primary-light-5: #193450;
+  --color-primary-light-6: #0c1a28;
+  --color-primary-light-7: #04080c;
+  --color-primary-alpha-10: #4183c419;
+  --color-primary-alpha-20: #4183c433;
+  --color-primary-alpha-30: #4183c44b;
+  --color-primary-alpha-40: #4183c466;
+  --color-primary-alpha-50: #4183c480;
+  --color-primary-alpha-60: #4183c499;
+  --color-primary-alpha-70: #4183c4b3;
+  --color-primary-alpha-80: #4183c4cc;
+  --color-primary-alpha-90: #4183c4e1;
   --color-primary-hover: var(--color-primary-light-1);
   --color-primary-active: var(--color-primary-light-2);
-  --color-secondary: #525767;
-  --color-secondary-dark-1: #5c6374;
-  --color-secondary-dark-2: #666e81;
-  --color-secondary-dark-3: #7c8497;
-  --color-secondary-dark-4: #8990a1;
-  --color-secondary-dark-5: #959cab;
-  --color-secondary-dark-6: #a2a8b5;
-  --color-secondary-dark-7: #afb4c0;
-  --color-secondary-dark-8: #bcc0ca;
-  --color-secondary-dark-9: #c9cbd4;
-  --color-secondary-dark-10: #d6d7de;
-  --color-secondary-dark-11: #e2e3e8;
-  --color-secondary-dark-12: #eeeff2;
-  --color-secondary-dark-13: #fbfbfc;
-  --color-secondary-light-1: #454a57;
-  --color-secondary-light-2: #383c47;
-  --color-secondary-light-3: #2c2f37;
-  --color-secondary-light-4: #1f2226;
-  --color-secondary-alpha-10: #52576719;
-  --color-secondary-alpha-20: #52576733;
-  --color-secondary-alpha-30: #5257674b;
-  --color-secondary-alpha-40: #52576766;
-  --color-secondary-alpha-50: #52576780;
-  --color-secondary-alpha-60: #52576799;
-  --color-secondary-alpha-70: #525767b3;
-  --color-secondary-alpha-80: #525767cc;
-  --color-secondary-alpha-90: #525767e1;
+  --color-secondary: #3b444c;
+  --color-secondary-dark-1: #414b54;
+  --color-secondary-dark-2: #49545f;
+  --color-secondary-dark-3: #576471;
+  --color-secondary-dark-4: #677685;
+  --color-secondary-dark-5: #758594;
+  --color-secondary-dark-6: #8392a0;
+  --color-secondary-dark-7: #929eab;
+  --color-secondary-dark-8: #a2acb7;
+  --color-secondary-dark-9: #a9b3bd;
+  --color-secondary-dark-10: #b7bfc7;
+  --color-secondary-dark-11: #c5cbd2;
+  --color-secondary-dark-12: #cfd4da;
+  --color-secondary-dark-13: #d2d7dc;
+  --color-secondary-light-1: #313940;
+  --color-secondary-light-2: #292f35;
+  --color-secondary-light-3: #1d2226;
+  --color-secondary-light-4: #171b1e;
+  --color-secondary-alpha-10: #3b444c19;
+  --color-secondary-alpha-20: #3b444c33;
+  --color-secondary-alpha-30: #3b444c4b;
+  --color-secondary-alpha-40: #3b444c66;
+  --color-secondary-alpha-50: #3b444c80;
+  --color-secondary-alpha-60: #3b444c99;
+  --color-secondary-alpha-70: #3b444cb3;
+  --color-secondary-alpha-80: #3b444ccc;
+  --color-secondary-alpha-90: #3b444ce1;
   --color-secondary-button: var(--color-secondary-dark-4);
   --color-secondary-hover: var(--color-secondary-dark-3);
   --color-secondary-active: var(--color-secondary-dark-2);
   /* console colors - used for actions console and console files */
-  --color-console-fg: #eeeff2;
-  --color-console-fg-subtle: #959cab;
-  --color-console-bg: #262936;
-  --color-console-border: #383c47;
-  --color-console-hover-bg: #ffffff16;
-  --color-console-active-bg: #454a57;
-  --color-console-menu-bg: #383c47;
-  --color-console-menu-border: #5c6374;
+  --color-console-fg: #f7f8f9;
+  --color-console-fg-subtle: #bdc4cc;
+  --color-console-bg: #171b1e;
+  --color-console-border: #2e353b;
+  --color-console-hover-bg: #272d33;
+  --color-console-active-bg: #2e353b;
+  --color-console-menu-bg: #262b31;
+  --color-console-menu-border: #414b55;
   /* named colors */
   --color-red: #cc4848;
   --color-orange: #cc580c;
@@ -81,7 +81,7 @@
   --color-purple: #b259d0;
   --color-pink: #d22e8b;
   --color-brown: #a47252;
-  --color-black: #2e323e;
+  --color-black: #1d2328;
   /* light variants - produced via Sass scale-color(color, $lightness: +10%) */
   --color-red-light: #d15a5a;
   --color-orange-light: #f6a066;
@@ -94,7 +94,7 @@
   --color-purple-light: #ba6ad5;
   --color-pink-light: #d74397;
   --color-brown-light: #b08061;
-  --color-black-light: #3f4555;
+  --color-black-light: #424851;
   /* dark 1 variants - produced via Sass scale-color(color, $lightness: -10%) */
   --color-red-dark-1: #c23636;
   --color-orange-dark-1: #f38236;
@@ -107,7 +107,7 @@
   --color-purple-dark-1: #a742c9;
   --color-pink-dark-1: #be297d;
   --color-brown-dark-1: #94674a;
-  --color-black-dark-1: #292d38;
+  --color-black-dark-1: #292e38;
   /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */
   --color-red-dark-2: #ad3030;
   --color-orange-dark-2: #f16e17;
@@ -120,27 +120,27 @@
   --color-purple-dark-2: #9834b9;
   --color-pink-dark-2: #a9246f;
   --color-brown-dark-2: #835b42;
-  --color-black-dark-2: #252832;
+  --color-black-dark-2: #272930;
   /* ansi colors used for actions console and console files */
-  --color-ansi-black: var(--color-black);
-  --color-ansi-red: var(--color-red);
-  --color-ansi-green: var(--color-green);
-  --color-ansi-yellow: var(--color-yellow);
-  --color-ansi-blue: var(--color-blue);
-  --color-ansi-magenta: var(--color-pink);
-  --color-ansi-cyan: var(--color-teal);
+  --color-ansi-black: #1e2327;
+  --color-ansi-red: #cc4848;
+  --color-ansi-green: #87ab63;
+  --color-ansi-yellow: #cc9903;
+  --color-ansi-blue: #3a8ac6;
+  --color-ansi-magenta: #d22e8b;
+  --color-ansi-cyan: #00918a;
   --color-ansi-white: var(--color-console-fg-subtle);
-  --color-ansi-bright-black: var(--color-black-light);
-  --color-ansi-bright-red: var(--color-red-light);
-  --color-ansi-bright-green: var(--color-green-light);
-  --color-ansi-bright-yellow: var(--color-yellow-light);
-  --color-ansi-bright-blue: var(--color-blue-light);
-  --color-ansi-bright-magenta: var(--color-pink-light);
-  --color-ansi-bright-cyan: var(--color-teal-light);
+  --color-ansi-bright-black: #424851;
+  --color-ansi-bright-red: #d15a5a;
+  --color-ansi-bright-green: #93b373;
+  --color-ansi-bright-yellow: #eaaf03;
+  --color-ansi-bright-blue: #4e96cc;
+  --color-ansi-bright-magenta: #d74397;
+  --color-ansi-bright-cyan: #00b6ad;
   --color-ansi-bright-white: var(--color-console-fg);
   /* other colors */
-  --color-grey: #505665;
-  --color-grey-light: #a1a6b7;
+  --color-grey: #384149;
+  --color-grey-light: #818f9e;
   --color-gold: #b1983b;
   --color-white: #ffffff;
   --color-diff-removed-word-bg: #6f3333;
@@ -151,7 +151,7 @@
   --color-diff-removed-row-border: #634343;
   --color-diff-moved-row-border: #bcca6f;
   --color-diff-added-row-border: #314a37;
-  --color-diff-inactive: #353846;
+  --color-diff-inactive: #22282d;
   --color-error-border: #a04141;
   --color-error-bg: #522;
   --color-error-bg-active: #744;
@@ -180,60 +180,59 @@
   --color-orange-badge-hover-bg: #f2711c4d;
   --color-git: #f05133;
   /* target-based colors */
-  --color-body: #2e323e;
-  --color-box-header: #303340;
-  --color-box-body: #222733;
-  --color-box-body-highlight: #262b36;
-  --color-text-dark: #dbe0ea;
-  --color-text: #cbd0da;
-  --color-text-light: #bbbfca;
-  --color-text-light-1: #aaafb9;
-  --color-text-light-2: #9a9ea9;
-  --color-text-light-3: #8a8e99;
-  --color-footer: #232834;
-  --color-timeline: #4c525e;
-  --color-input-text: #dfe3ec;
-  --color-input-background: #1e252e;
-  --color-input-toggle-background: #454a57;
+  --color-body: #1b1f23;
+  --color-box-header: #1a1d1f;
+  --color-box-body: #14171a;
+  --color-box-body-highlight: #1e2226;
+  --color-text-dark: #f7f8f9;
+  --color-text: #d0d5da;
+  --color-text-light: #bcc3cb;
+  --color-text-light-1: #a5afb9;
+  --color-text-light-2: #8f9ba8;
+  --color-text-light-3: #788797;
+  --color-footer: var(--color-nav-bg);
+  --color-timeline: #343c44;
+  --color-input-text: var(--color-text-dark);
+  --color-input-background: #171a1e;
+  --color-input-toggle-background: #2e353c;
   --color-input-border: var(--color-secondary);
   --color-input-border-hover: var(--color-secondary-dark-1);
-  --color-header-wrapper: #202430;
-  --color-light: #00000028;
+  --color-light: #00001728;
   --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled)));
-  --color-light-border: #ffffff28;
-  --color-hover: #ffffff19;
-  --color-active: #ffffff24;
-  --color-menu: #1e252e;
-  --color-card: #1e252e;
-  --color-markup-table-row: #ffffff06;
-  --color-markup-code-block: #ffffff16;
-  --color-button: #1e252e;
-  --color-code-bg: #222733;
-  --color-code-sidebar-bg: #232834;
-  --color-shadow: #00000058;
-  --color-secondary-bg: #2a2e3a;
-  --color-expand-button: #3c404d;
-  --color-placeholder-text: #8a8e99;
+  --color-light-border: #e8f3ff28;
+  --color-hover: #e8f3ff19;
+  --color-active: #e8f3ff24;
+  --color-menu: #171a1e;
+  --color-card: #171a1e;
+  --color-markup-table-row: #e8f3ff0f;
+  --color-markup-code-block: #e8f3ff12;
+  --color-markup-code-inline: #e8f3ff28;
+  --color-button: #171a1e;
+  --color-code-bg: #14171a;
+  --color-shadow: #00001758;
+  --color-secondary-bg: #2a3137;
+  --color-expand-button: #2f363d;
+  --color-placeholder-text: var(--color-text-light-3);
   --color-editor-line-highlight: var(--color-primary-light-5);
   --color-project-board-bg: var(--color-secondary-light-2);
-  --color-project-board-dark-label: #111111;
-  --color-project-board-light-label: #eeeeee;
   --color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
-  --color-reaction-bg: #ffffff12;
+  --color-reaction-bg: #e8f3ff12;
   --color-reaction-hover-bg: var(--color-primary-light-4);
   --color-reaction-active-bg: var(--color-primary-light-5);
-  --color-tooltip-text: #ffffff;
-  --color-tooltip-bg: #000000f0;
-  --color-nav-bg: #232834;
-  --color-nav-hover-bg: #383c47;
+  --color-tooltip-text: #f9fafb;
+  --color-tooltip-bg: #000b17f0;
+  --color-nav-bg: #16191d;
+  --color-nav-hover-bg: var(--color-secondary-light-1);
   --color-nav-text: var(--color-text);
-  --color-label-text: #dfe3ec;
-  --color-label-bg: #7c84974b;
-  --color-label-hover-bg: #7c8497a0;
-  --color-label-active-bg: #7c8497ff;
+  --color-secondary-nav-bg: #181c20;
+  --color-label-text: var(--color-text);
+  --color-label-bg: #7282924b;
+  --color-label-hover-bg: #728292a0;
+  --color-label-active-bg: #728292ff;
   --color-accent: var(--color-primary-light-1);
   --color-small-accent: var(--color-primary-light-5);
-  --color-active-line: #534d1b;
+  --color-highlight-fg: #87651e;
+  --color-highlight-bg: #352c1c;
   --color-overlay-backdrop: #080808c0;
   accent-color: var(--color-accent);
   color-scheme: dark;
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index ca5d15cd25..dfccd37647 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -30,45 +30,45 @@
   --color-primary-alpha-90: #4183c4e1;
   --color-primary-hover: var(--color-primary-dark-1);
   --color-primary-active: var(--color-primary-dark-2);
-  --color-secondary: #dedede;
-  --color-secondary-dark-1: #cecece;
-  --color-secondary-dark-2: #bfbfbf;
-  --color-secondary-dark-3: #a0a0a0;
-  --color-secondary-dark-4: #909090;
-  --color-secondary-dark-5: #818181;
-  --color-secondary-dark-6: #717171;
-  --color-secondary-dark-7: #626262;
-  --color-secondary-dark-8: #525252;
-  --color-secondary-dark-9: #434343;
-  --color-secondary-dark-10: #333333;
-  --color-secondary-dark-11: #242424;
-  --color-secondary-dark-12: #141414;
-  --color-secondary-dark-13: #040404;
-  --color-secondary-light-1: #e5e5e5;
-  --color-secondary-light-2: #ebebeb;
-  --color-secondary-light-3: #f2f2f2;
-  --color-secondary-light-4: #f8f8f8;
-  --color-secondary-alpha-10: #dedede19;
-  --color-secondary-alpha-20: #dedede33;
-  --color-secondary-alpha-30: #dedede4b;
-  --color-secondary-alpha-40: #dedede66;
-  --color-secondary-alpha-50: #dedede80;
-  --color-secondary-alpha-60: #dedede99;
-  --color-secondary-alpha-70: #dededeb3;
-  --color-secondary-alpha-80: #dededecc;
-  --color-secondary-alpha-90: #dededee1;
+  --color-secondary: #d0d7de;
+  --color-secondary-dark-1: #c7ced5;
+  --color-secondary-dark-2: #b9c0c7;
+  --color-secondary-dark-3: #99a0a7;
+  --color-secondary-dark-4: #899097;
+  --color-secondary-dark-5: #7a8188;
+  --color-secondary-dark-6: #6a7178;
+  --color-secondary-dark-7: #5b6269;
+  --color-secondary-dark-8: #4b5259;
+  --color-secondary-dark-9: #3c434a;
+  --color-secondary-dark-10: #2c333a;
+  --color-secondary-dark-11: #1d242b;
+  --color-secondary-dark-12: #0d141b;
+  --color-secondary-dark-13: #00040b;
+  --color-secondary-light-1: #dee5ec;
+  --color-secondary-light-2: #e4ebf2;
+  --color-secondary-light-3: #ebf2f9;
+  --color-secondary-light-4: #f1f8ff;
+  --color-secondary-alpha-10: #d0d7de19;
+  --color-secondary-alpha-20: #d0d7de33;
+  --color-secondary-alpha-30: #d0d7de4b;
+  --color-secondary-alpha-40: #d0d7de66;
+  --color-secondary-alpha-50: #d0d7de80;
+  --color-secondary-alpha-60: #d0d7de99;
+  --color-secondary-alpha-70: #d0d7deb3;
+  --color-secondary-alpha-80: #d0d7decc;
+  --color-secondary-alpha-90: #d0d7dee1;
   --color-secondary-button: var(--color-secondary-dark-4);
   --color-secondary-hover: var(--color-secondary-dark-5);
   --color-secondary-active: var(--color-secondary-dark-6);
   /* console colors - used for actions console and console files */
-  --color-console-fg: #eeeff2;
-  --color-console-fg-subtle: #959cab;
-  --color-console-bg: #262936;
-  --color-console-border: #383c47;
-  --color-console-hover-bg: #ffffff16;
-  --color-console-active-bg: #454a57;
-  --color-console-menu-bg: #383c47;
-  --color-console-menu-border: #5c6374;
+  --color-console-fg: #f7f8f9;
+  --color-console-fg-subtle: #bdc4cc;
+  --color-console-bg: #171b1e;
+  --color-console-border: #2e353b;
+  --color-console-hover-bg: #272d33;
+  --color-console-active-bg: #2e353b;
+  --color-console-menu-bg: #262b31;
+  --color-console-menu-border: #414b55;
   /* named colors */
   --color-red: #db2828;
   --color-orange: #f2711c;
@@ -81,7 +81,7 @@
   --color-purple: #a333c8;
   --color-pink: #e03997;
   --color-brown: #a5673f;
-  --color-black: #1b1c1d;
+  --color-black: #1d2328;
   /* light variants - produced via Sass scale-color(color, $lightness: +25%) */
   --color-red-light: #e45e5e;
   --color-orange-light: #f59555;
@@ -94,7 +94,7 @@
   --color-purple-light: #bb64d8;
   --color-pink-light: #e86bb1;
   --color-brown-light: #c58b66;
-  --color-black-light: #525558;
+  --color-black-light: #4b5b68;
   /* dark 1 variants - produced via Sass scale-color(color, $lightness: -10%) */
   --color-red-dark-1: #c82121;
   --color-orange-dark-1: #e6630d;
@@ -107,7 +107,7 @@
   --color-purple-dark-1: #932eb4;
   --color-pink-dark-1: #db228a;
   --color-brown-dark-1: #955d39;
-  --color-black-dark-1: #18191a;
+  --color-black-dark-1: #2c3339;
   /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */
   --color-red-dark-2: #b11e1e;
   --color-orange-dark-2: #cc580c;
@@ -120,27 +120,27 @@
   --color-purple-dark-2: #8229a0;
   --color-pink-dark-2: #c21e7b;
   --color-brown-dark-2: #845232;
-  --color-black-dark-2: #161617;
+  --color-black-dark-2: #131619;
   /* ansi colors used for actions console and console files */
-  --color-ansi-black: var(--color-black);
-  --color-ansi-red: var(--color-red);
-  --color-ansi-green: var(--color-green);
-  --color-ansi-yellow: var(--color-yellow);
-  --color-ansi-blue: var(--color-blue);
-  --color-ansi-magenta: var(--color-pink);
-  --color-ansi-cyan: var(--color-teal);
+  --color-ansi-black: #1e2327;
+  --color-ansi-red: #cc4848;
+  --color-ansi-green: #87ab63;
+  --color-ansi-yellow: #cc9903;
+  --color-ansi-blue: #3a8ac6;
+  --color-ansi-magenta: #d22e8b;
+  --color-ansi-cyan: #00918a;
   --color-ansi-white: var(--color-console-fg-subtle);
-  --color-ansi-bright-black: var(--color-black-light);
-  --color-ansi-bright-red: var(--color-red-light);
-  --color-ansi-bright-green: var(--color-green-light);
-  --color-ansi-bright-yellow: var(--color-yellow-light);
-  --color-ansi-bright-blue: var(--color-blue-light);
-  --color-ansi-bright-magenta: var(--color-pink-light);
-  --color-ansi-bright-cyan: var(--color-teal-light);
+  --color-ansi-bright-black: #46494d;
+  --color-ansi-bright-red: #d15a5a;
+  --color-ansi-bright-green: #93b373;
+  --color-ansi-bright-yellow: #eaaf03;
+  --color-ansi-bright-blue: #4e96cc;
+  --color-ansi-bright-magenta: #d74397;
+  --color-ansi-bright-cyan: #00b6ad;
   --color-ansi-bright-white: var(--color-console-fg);
   /* other colors */
-  --color-grey: #707070;
-  --color-grey-light: #838383;
+  --color-grey: #697077;
+  --color-grey-light: #7c838a;
   --color-gold: #a1882b;
   --color-white: #ffffff;
   --color-diff-removed-word-bg: #fdb8c0;
@@ -151,7 +151,7 @@
   --color-diff-removed-row-border: #f1c0c0;
   --color-diff-moved-row-border: #d0e27f;
   --color-diff-added-row-border: #e6ffed;
-  --color-diff-inactive: #f2f2f2;
+  --color-diff-inactive: #f0f2f4;
   --color-error-border: #e0b4b4;
   --color-error-bg: #fff6f6;
   --color-error-bg-active: #fbb;
@@ -181,59 +181,58 @@
   --color-git: #f05133;
   /* target-based colors */
   --color-body: #ffffff;
-  --color-box-header: #f7f7f7;
+  --color-box-header: #f1f3f5;
   --color-box-body: #ffffff;
-  --color-box-body-highlight: #fafafa;
-  --color-text-dark: #080808;
-  --color-text: #212121;
-  --color-text-light: #555555;
-  --color-text-light-1: #6a6a6a;
-  --color-text-light-2: #808080;
-  --color-text-light-3: #a0a0a0;
-  --color-footer: #ffffff;
-  --color-timeline: #ececec;
-  --color-input-text: #212121;
-  --color-input-background: #fafafa;
-  --color-input-toggle-background: #dedede;
+  --color-box-body-highlight: #ecf5fd;
+  --color-text-dark: #01050a;
+  --color-text: #181c21;
+  --color-text-light: #30363b;
+  --color-text-light-1: #40474d;
+  --color-text-light-2: #5b6167;
+  --color-text-light-3: #747c84;
+  --color-footer: var(--color-nav-bg);
+  --color-timeline: #d0d7de;
+  --color-input-text: var(--color-text-dark);
+  --color-input-background: #fff;
+  --color-input-toggle-background: #d0d7de;
   --color-input-border: var(--color-secondary);
   --color-input-border-hover: var(--color-secondary-dark-1);
-  --color-header-wrapper: transparent;
-  --color-light: #00000006;
+  --color-light: #00001706;
   --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled)));
-  --color-light-border: #0000001d;
-  --color-hover: #00000014;
-  --color-active: #0000001b;
-  --color-menu: #fafafa;
-  --color-card: #fafafa;
-  --color-markup-table-row: #00000008;
-  --color-markup-code-block: #00000010;
-  --color-button: #fafafa;
-  --color-code-bg: #ffffff;
-  --color-code-sidebar-bg: #f5f5f5;
-  --color-shadow: #00000026;
-  --color-secondary-bg: #f4f4f4;
-  --color-expand-button: #d8efff;
-  --color-placeholder-text: #aaa;
+  --color-light-border: #0000171d;
+  --color-hover: #00001708;
+  --color-active: #00001714;
+  --color-menu: #f8f9fb;
+  --color-card: #f8f9fb;
+  --color-markup-table-row: #0030600a;
+  --color-markup-code-block: #00306010;
+  --color-markup-code-inline: #00306012;
+  --color-button: #f8f9fb;
+  --color-code-bg: #fafdff;
+  --color-shadow: #00001726;
+  --color-secondary-bg: #f2f5f8;
+  --color-expand-button: #cfe8fa;
+  --color-placeholder-text: var(--color-text-light-3);
   --color-editor-line-highlight: var(--color-primary-light-6);
   --color-project-board-bg: var(--color-secondary-light-4);
-  --color-project-board-dark-label: #111111;
-  --color-project-board-light-label: #eeeeee;
   --color-caret: var(--color-text-dark);
-  --color-reaction-bg: #0000000a;
+  --color-reaction-bg: #0000170a;
   --color-reaction-hover-bg: var(--color-primary-light-5);
   --color-reaction-active-bg: var(--color-primary-light-6);
-  --color-tooltip-text: #ffffff;
-  --color-tooltip-bg: #000000f0;
-  --color-nav-bg: #ffffff;
-  --color-nav-hover-bg: #ebebeb;
+  --color-tooltip-text: #fbfdff;
+  --color-tooltip-bg: #000017f0;
+  --color-nav-bg: #f6f7fa;
+  --color-nav-hover-bg: var(--color-secondary-light-1);
   --color-nav-text: var(--color-text);
-  --color-label-text: #232323;
-  --color-label-bg: #cacaca5b;
-  --color-label-hover-bg: #cacacaa0;
-  --color-label-active-bg: #cacacaff;
+  --color-secondary-nav-bg: #f9fafb;
+  --color-label-text: var(--color-text);
+  --color-label-bg: #949da64b;
+  --color-label-hover-bg: #949da6a0;
+  --color-label-active-bg: #949da6ff;
   --color-accent: var(--color-primary-light-1);
   --color-small-accent: var(--color-primary-light-6);
-  --color-active-line: #fffbdd;
+  --color-highlight-fg: #eed200;
+  --color-highlight-bg: #fffbdd;
   --color-overlay-backdrop: #080808c0;
   accent-color: var(--color-accent);
   color-scheme: light;
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index ad3f13bb58..49c00c4dad 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -156,7 +156,6 @@
   width: 1.28571429em;
   height: 1.28571429em;
   border-radius: 500rem;
-  -webkit-animation: loader 0.6s infinite linear;
   animation: loader 0.6s infinite linear;
   border: 0.2em solid currentColor;
   color: #FFFFFF;
@@ -530,7 +529,6 @@
   border-top-left-radius: inherit;
   border-bottom-left-radius: inherit;
   text-align: center;
-  -webkit-animation: none;
   animation: none;
   padding: 0.78571429em 0 0.78571429em 0;
   margin: 0;
@@ -594,7 +592,6 @@
 /* Loading Icon in Labeled Button */
 
 .ui.labeled.icon.button > .loading.icon:before {
-  -webkit-animation: loader 2s linear infinite;
   animation: loader 2s linear infinite;
 }
 
@@ -2326,873 +2323,6 @@
          Theme Overrides
 *******************************/
 
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Checkbox
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-           Checkbox
-*******************************/
-
-/*--------------
-    Content
----------------*/
-
-.ui.checkbox {
-  position: relative;
-  display: inline-block;
-  -webkit-backface-visibility: hidden;
-  backface-visibility: hidden;
-  outline: none;
-  vertical-align: baseline;
-  font-style: normal;
-  min-height: 17px;
-  font-size: 1em;
-  line-height: 17px;
-  min-width: 17px;
-}
-
-/* HTML Checkbox */
-
-.ui.checkbox input[type="checkbox"],
-.ui.checkbox input[type="radio"] {
-  cursor: pointer;
-  position: absolute;
-  top: 0;
-  left: 0;
-  opacity: 0 !important;
-  outline: none;
-  z-index: 3;
-  width: 17px;
-  height: 17px;
-}
-
-.ui.checkbox label {
-  cursor: auto;
-  position: relative;
-  display: block;
-  padding-left: 1.85714em;
-  outline: none;
-  font-size: 1em;
-}
-
-.ui.checkbox label:before {
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 17px;
-  height: 17px;
-  content: '';
-  background: #FFFFFF;
-  border-radius: 0.21428571rem;
-  transition: border 0.1s ease, opacity 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease;
-  border: 1px solid #D4D4D5;
-}
-
-/*--------------
-    Checkmark
----------------*/
-
-.ui.checkbox label:after {
-  position: absolute;
-  font-size: 14px;
-  top: 0;
-  left: 0;
-  width: 17px;
-  height: 17px;
-  text-align: center;
-  opacity: 0;
-  color: rgba(0, 0, 0, 0.87);
-  transition: border 0.1s ease, opacity 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease;
-}
-
-/*--------------
-      Label
----------------*/
-
-/* Inside */
-
-.ui.checkbox label,
-.ui.checkbox + label {
-  color: rgba(0, 0, 0, 0.87);
-  transition: color 0.1s ease;
-}
-
-/* Outside */
-
-.ui.checkbox + label {
-  vertical-align: middle;
-}
-
-/*******************************
-           States
-*******************************/
-
-/*--------------
-      Hover
----------------*/
-
-.ui.checkbox label:hover::before {
-  background: #FFFFFF;
-  border-color: rgba(34, 36, 38, 0.35);
-}
-
-.ui.checkbox label:hover,
-.ui.checkbox + label:hover {
-  color: rgba(0, 0, 0, 0.8);
-}
-
-/*--------------
-      Down
----------------*/
-
-.ui.checkbox label:active::before {
-  background: #F9FAFB;
-  border-color: rgba(34, 36, 38, 0.35);
-}
-
-.ui.checkbox label:active::after {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.checkbox input:active ~ label {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*--------------
-     Focus
----------------*/
-
-.ui.checkbox input:focus ~ label:before {
-  background: #FFFFFF;
-  border-color: #96C8DA;
-}
-
-.ui.checkbox input:focus ~ label:after {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.checkbox input:focus ~ label {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*--------------
-     Active
----------------*/
-
-.ui.checkbox input:checked ~ label:before {
-  background: #FFFFFF;
-  border-color: rgba(34, 36, 38, 0.35);
-}
-
-.ui.checkbox input:checked ~ label:after {
-  opacity: 1;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*--------------
-    Indeterminate
-  ---------------*/
-
-.ui.checkbox input:not([type=radio]):indeterminate ~ label:before {
-  background: #FFFFFF;
-  border-color: rgba(34, 36, 38, 0.35);
-}
-
-.ui.checkbox input:not([type=radio]):indeterminate ~ label:after {
-  opacity: 1;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.indeterminate.toggle.checkbox input:not([type=radio]):indeterminate ~ label:before {
-  background: rgba(0, 0, 0, 0.15);
-}
-
-.ui.indeterminate.toggle.checkbox input:not([type=radio]) ~ label:after {
-  left: 1.075rem;
-}
-
-/*--------------
-  Active Focus
----------------*/
-
-.ui.checkbox input:not([type=radio]):indeterminate:focus ~ label:before,
-.ui.checkbox input:checked:focus ~ label:before {
-  background: #FFFFFF;
-  border-color: #96C8DA;
-}
-
-.ui.checkbox input:not([type=radio]):indeterminate:focus ~ label:after,
-.ui.checkbox input:checked:focus ~ label:after {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*--------------
-    Read-Only
----------------*/
-
-.ui.read-only.checkbox,
-.ui.read-only.checkbox label {
-  cursor: default;
-}
-
-/*--------------
-       Disabled
-  ---------------*/
-
-.ui.disabled.checkbox label,
-.ui.checkbox input[disabled] ~ label {
-  cursor: default !important;
-  opacity: 0.5;
-  color: #000000;
-  pointer-events: none;
-}
-
-/*--------------
-     Hidden
----------------*/
-
-/* Initialized checkbox moves input below element
- to prevent manually triggering */
-
-.ui.checkbox input.hidden {
-  z-index: -1;
-}
-
-/* Selectable Label */
-
-.ui.checkbox input.hidden + label {
-  cursor: pointer;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  user-select: none;
-}
-
-/*******************************
-             Types
-*******************************/
-
-/*--------------
-       Radio
-  ---------------*/
-
-.ui.radio.checkbox {
-  min-height: 15px;
-}
-
-.ui.radio.checkbox label {
-  padding-left: 1.85714em;
-}
-
-/* Box */
-
-.ui.radio.checkbox label:before {
-  content: '';
-  transform: none;
-  width: 15px;
-  height: 15px;
-  border-radius: 500rem;
-  top: 1px;
-  left: 0;
-}
-
-/* Bullet */
-
-.ui.radio.checkbox label:after {
-  border: none;
-  content: '' !important;
-  line-height: 15px;
-  top: 1px;
-  left: 0;
-  width: 15px;
-  height: 15px;
-  border-radius: 500rem;
-  transform: scale(0.46666667);
-  background-color: rgba(0, 0, 0, 0.87);
-}
-
-/* Focus */
-
-.ui.radio.checkbox input:focus ~ label:before {
-  background-color: #FFFFFF;
-}
-
-.ui.radio.checkbox input:focus ~ label:after {
-  background-color: rgba(0, 0, 0, 0.95);
-}
-
-/* Indeterminate */
-
-.ui.radio.checkbox input:indeterminate ~ label:after {
-  opacity: 0;
-}
-
-/* Active */
-
-.ui.radio.checkbox input:checked ~ label:before {
-  background-color: #FFFFFF;
-}
-
-.ui.radio.checkbox input:checked ~ label:after {
-  background-color: rgba(0, 0, 0, 0.95);
-}
-
-/* Active Focus */
-
-.ui.radio.checkbox input:focus:checked ~ label:before {
-  background-color: #FFFFFF;
-}
-
-.ui.radio.checkbox input:focus:checked ~ label:after {
-  background-color: rgba(0, 0, 0, 0.95);
-}
-
-/*--------------
-       Slider
-  ---------------*/
-
-.ui.slider.checkbox {
-  min-height: 1.25rem;
-}
-
-/* Input */
-
-.ui.slider.checkbox input {
-  width: 3.5rem;
-  height: 1.25rem;
-}
-
-/* Label */
-
-.ui.slider.checkbox label {
-  padding-left: 4.5rem;
-  line-height: 1rem;
-  color: rgba(0, 0, 0, 0.4);
-}
-
-/* Line */
-
-.ui.slider.checkbox label:before {
-  display: block;
-  position: absolute;
-  content: '';
-  transform: none;
-  border: none !important;
-  left: 0;
-  z-index: 1;
-  top: 0.4rem;
-  background-color: rgba(0, 0, 0, 0.05);
-  width: 3.5rem;
-  height: 0.21428571rem;
-  border-radius: 500rem;
-  transition: background 0.3s ease;
-}
-
-/* Handle */
-
-.ui.slider.checkbox label:after {
-  background: #FFFFFF linear-gradient(transparent, rgba(0, 0, 0, 0.05));
-  position: absolute;
-  content: '' !important;
-  opacity: 1;
-  z-index: 2;
-  border: none;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
-  width: 1.5rem;
-  height: 1.5rem;
-  top: -0.25rem;
-  left: 0;
-  transform: none;
-  border-radius: 500rem;
-  transition: left 0.3s ease;
-}
-
-/* Focus */
-
-.ui.slider.checkbox input:focus ~ label:before {
-  background-color: rgba(0, 0, 0, 0.15);
-  border: none;
-}
-
-/* Hover */
-
-.ui.slider.checkbox label:hover {
-  color: rgba(0, 0, 0, 0.8);
-}
-
-.ui.slider.checkbox label:hover::before {
-  background: rgba(0, 0, 0, 0.15);
-}
-
-/* Active */
-
-.ui.slider.checkbox input:checked ~ label {
-  color: rgba(0, 0, 0, 0.95) !important;
-}
-
-.ui.slider.checkbox input:checked ~ label:before {
-  background-color: #545454 !important;
-}
-
-.ui.slider.checkbox input:checked ~ label:after {
-  left: 2rem;
-}
-
-/* Active Focus */
-
-.ui.slider.checkbox input:focus:checked ~ label {
-  color: rgba(0, 0, 0, 0.95) !important;
-}
-
-.ui.slider.checkbox input:focus:checked ~ label:before {
-  background-color: #000000 !important;
-}
-
-/*--------------
-       Toggle
-  ---------------*/
-
-.ui.toggle.checkbox {
-  min-height: 1.5rem;
-}
-
-/* Input */
-
-.ui.toggle.checkbox input {
-  width: 3.5rem;
-  height: 1.5rem;
-}
-
-/* Label */
-
-.ui.toggle.checkbox label {
-  min-height: 1.5rem;
-  padding-left: 4.5rem;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-.ui.toggle.checkbox label {
-  padding-top: 0.15em;
-}
-
-/* Switch */
-
-.ui.toggle.checkbox label:before {
-  display: block;
-  position: absolute;
-  content: '';
-  z-index: 1;
-  transform: none;
-  border: none;
-  top: 0;
-  background: rgba(0, 0, 0, 0.05);
-  box-shadow: none;
-  width: 3.5rem;
-  height: 1.5rem;
-  border-radius: 500rem;
-}
-
-/* Handle */
-
-.ui.toggle.checkbox label:after {
-  background: #FFFFFF linear-gradient(transparent, rgba(0, 0, 0, 0.05));
-  position: absolute;
-  content: '' !important;
-  opacity: 1;
-  z-index: 2;
-  border: none;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
-  width: 1.5rem;
-  height: 1.5rem;
-  top: 0;
-  left: 0;
-  border-radius: 500rem;
-  transition: background 0.3s ease, left 0.3s ease;
-}
-
-.ui.toggle.checkbox input ~ label:after {
-  left: -0.05rem;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
-}
-
-/* Focus */
-
-.ui.toggle.checkbox input:focus ~ label:before {
-  background-color: rgba(0, 0, 0, 0.15);
-  border: none;
-}
-
-/* Hover */
-
-.ui.toggle.checkbox label:hover::before {
-  background-color: rgba(0, 0, 0, 0.15);
-  border: none;
-}
-
-/* Active */
-
-.ui.toggle.checkbox input:checked ~ label {
-  color: rgba(0, 0, 0, 0.95) !important;
-}
-
-.ui.toggle.checkbox input:checked ~ label:before {
-  background-color: #2185D0 !important;
-}
-
-.ui.toggle.checkbox input:checked ~ label:after {
-  left: 2.15rem;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
-}
-
-/* Active Focus */
-
-.ui.toggle.checkbox input:focus:checked ~ label {
-  color: rgba(0, 0, 0, 0.95) !important;
-}
-
-.ui.toggle.checkbox input:focus:checked ~ label:before {
-  background-color: #0d71bb !important;
-}
-
-/*******************************
-            Variations
-*******************************/
-
-/*--------------
-       Fitted
-  ---------------*/
-
-.ui.fitted.checkbox label {
-  padding-left: 0 !important;
-}
-
-.ui.fitted.toggle.checkbox {
-  width: 3.5rem;
-}
-
-.ui.fitted.slider.checkbox {
-  width: 3.5rem;
-}
-
-/*--------------------
-        Size
----------------------*/
-
-.ui.mini.checkbox {
-  font-size: 0.78571429em;
-}
-
-.ui.tiny.checkbox {
-  font-size: 0.85714286em;
-}
-
-.ui.small.checkbox {
-  font-size: 0.92857143em;
-}
-
-.ui.large.checkbox {
-  font-size: 1.14285714em;
-}
-
-.ui.large.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.large.checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.large.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before,
-.ui.large.checkbox:not(.slider):not(.toggle):not(.radio) label:before {
-  transform: scale(1.14285714);
-  transform-origin: left;
-}
-
-.ui.large.form .checkbox.radio label:before,
-.ui.large.checkbox.radio label:before {
-  transform: scale(1.14285714);
-  transform-origin: left;
-}
-
-.ui.large.form .checkbox.radio label:after,
-.ui.large.checkbox.radio label:after {
-  transform: scale(0.57142857);
-  transform-origin: left;
-  left: 0.33571429em;
-}
-
-.ui.big.checkbox {
-  font-size: 1.28571429em;
-}
-
-.ui.big.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.big.checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.big.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before,
-.ui.big.checkbox:not(.slider):not(.toggle):not(.radio) label:before {
-  transform: scale(1.28571429);
-  transform-origin: left;
-}
-
-.ui.big.form .checkbox.radio label:before,
-.ui.big.checkbox.radio label:before {
-  transform: scale(1.28571429);
-  transform-origin: left;
-}
-
-.ui.big.form .checkbox.radio label:after,
-.ui.big.checkbox.radio label:after {
-  transform: scale(0.64285714);
-  transform-origin: left;
-  left: 0.37142857em;
-}
-
-.ui.huge.checkbox {
-  font-size: 1.42857143em;
-}
-
-.ui.huge.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.huge.checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.huge.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before,
-.ui.huge.checkbox:not(.slider):not(.toggle):not(.radio) label:before {
-  transform: scale(1.42857143);
-  transform-origin: left;
-}
-
-.ui.huge.form .checkbox.radio label:before,
-.ui.huge.checkbox.radio label:before {
-  transform: scale(1.42857143);
-  transform-origin: left;
-}
-
-.ui.huge.form .checkbox.radio label:after,
-.ui.huge.checkbox.radio label:after {
-  transform: scale(0.71428571);
-  transform-origin: left;
-  left: 0.40714286em;
-}
-
-.ui.massive.checkbox {
-  font-size: 1.71428571em;
-}
-
-.ui.massive.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.massive.checkbox:not(.slider):not(.toggle):not(.radio) label:after,
-.ui.massive.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before,
-.ui.massive.checkbox:not(.slider):not(.toggle):not(.radio) label:before {
-  transform: scale(1.71428571);
-  transform-origin: left;
-}
-
-.ui.massive.form .checkbox.radio label:before,
-.ui.massive.checkbox.radio label:before {
-  transform: scale(1.71428571);
-  transform-origin: left;
-}
-
-.ui.massive.form .checkbox.radio label:after,
-.ui.massive.checkbox.radio label:after {
-  transform: scale(0.85714286);
-  transform-origin: left;
-  left: 0.47857143em;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
-@font-face {
-  font-family: 'Checkbox';
-  src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBD8AAAC8AAAAYGNtYXAYVtCJAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5Zn4huwUAAAF4AAABYGhlYWQGPe1ZAAAC2AAAADZoaGVhB30DyAAAAxAAAAAkaG10eBBKAEUAAAM0AAAAHGxvY2EAmgESAAADUAAAABBtYXhwAAkALwAAA2AAAAAgbmFtZSC8IugAAAOAAAABknBvc3QAAwAAAAAFFAAAACAAAwMTAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADoAgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6AL//f//AAAAAAAg6AD//f//AAH/4xgEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAEUAUQO7AvgAGgAAARQHAQYjIicBJjU0PwE2MzIfAQE2MzIfARYVA7sQ/hQQFhcQ/uMQEE4QFxcQqAF2EBcXEE4QAnMWEP4UEBABHRAXFhBOEBCoAXcQEE4QFwAAAAABAAABbgMlAkkAFAAAARUUBwYjISInJj0BNDc2MyEyFxYVAyUQEBf9SRcQEBAQFwK3FxAQAhJtFxAQEBAXbRcQEBAQFwAAAAABAAAASQMlA24ALAAAARUUBwYrARUUBwYrASInJj0BIyInJj0BNDc2OwE1NDc2OwEyFxYdATMyFxYVAyUQEBfuEBAXbhYQEO4XEBAQEBfuEBAWbhcQEO4XEBACEm0XEBDuFxAQEBAX7hAQF20XEBDuFxAQEBAX7hAQFwAAAQAAAAIAAHRSzT9fDzz1AAsEAAAAAADRsdR3AAAAANGx1HcAAAAAA7sDbgAAAAgAAgAAAAAAAAABAAADwP/AAAAEAAAAAAADuwABAAAAAAAAAAAAAAAAAAAABwQAAAAAAAAAAAAAAAIAAAAEAABFAyUAAAMlAAAAAAAAAAoAFAAeAE4AcgCwAAEAAAAHAC0AAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAIAAAAAQAAAAAAAgAHAGkAAQAAAAAAAwAIADkAAQAAAAAABAAIAH4AAQAAAAAABQALABgAAQAAAAAABgAIAFEAAQAAAAAACgAaAJYAAwABBAkAAQAQAAgAAwABBAkAAgAOAHAAAwABBAkAAwAQAEEAAwABBAkABAAQAIYAAwABBAkABQAWACMAAwABBAkABgAQAFkAAwABBAkACgA0ALBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhWZXJzaW9uIDIuMABWAGUAcgBzAGkAbwBuACAAMgAuADBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhDaGVja2JveABDAGgAZQBjAGsAYgBvAHhSZWd1bGFyAFIAZQBnAHUAbABhAHJDaGVja2JveABDAGgAZQBjAGsAYgBvAHhGb250IGdlbmVyYXRlZCBieSBJY29Nb29uLgBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('truetype');
-}
-
-/* Checkmark */
-
-.ui.checkbox label:after,
-.ui.checkbox .box:after {
-  font-family: 'Checkbox';
-}
-
-/* Checked */
-
-.ui.checkbox input:checked ~ .box:after,
-.ui.checkbox input:checked ~ label:after {
-  content: '\e800';
-}
-
-/* Indeterminate */
-
-.ui.checkbox input:indeterminate ~ .box:after,
-.ui.checkbox input:indeterminate ~ label:after {
-  font-size: 12px;
-  content: '\e801';
-}
-
-/*  UTF Reference
-.check:before { content: '\e800'; }
-.dash:before  { content: '\e801'; }
-.plus:before { content: '\e802'; }
-*/
-
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Container
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            Container
-*******************************/
-
-/* All Sizes */
-
-.ui.container {
-  display: block;
-  max-width: 100%;
-}
-
-/* Mobile */
-
-@media only screen and (max-width: 767.98px) {
-  .ui.ui.ui.container:not(.fluid) {
-    width: auto;
-    margin-left: 1em;
-    margin-right: 1em;
-  }
-
-  .ui.ui.ui.grid.container {
-    width: auto;
-  }
-
-  .ui.ui.ui.relaxed.grid.container {
-    width: auto;
-  }
-
-  .ui.ui.ui.very.relaxed.grid.container {
-    width: auto;
-  }
-}
-
-/* Tablet */
-
-@media only screen and (min-width: 768px) and (max-width: 991.98px) {
-  .ui.ui.ui.container:not(.fluid) {
-    width: 723px;
-    margin-left: auto;
-    margin-right: auto;
-  }
-
-  .ui.ui.ui.grid.container {
-    width: calc(723px + 2rem);
-  }
-
-  .ui.ui.ui.relaxed.grid.container {
-    width: calc(723px + 3rem);
-  }
-
-  .ui.ui.ui.very.relaxed.grid.container {
-    width: calc(723px + 5rem);
-  }
-}
-
-/* Small Monitor */
-
-@media only screen and (min-width: 992px) and (max-width: 1199.98px) {
-  .ui.ui.ui.container:not(.fluid) {
-    width: 933px;
-    margin-left: auto;
-    margin-right: auto;
-  }
-
-  .ui.ui.ui.grid.container {
-    width: calc(933px + 2rem);
-  }
-
-  .ui.ui.ui.relaxed.grid.container {
-    width: calc(933px + 3rem);
-  }
-
-  .ui.ui.ui.very.relaxed.grid.container {
-    width: calc(933px + 5rem);
-  }
-}
-
-/* Large Monitor */
-
-@media only screen and (min-width: 1200px) {
-  .ui.ui.ui.container:not(.fluid) {
-    width: 1127px;
-    margin-left: auto;
-    margin-right: auto;
-  }
-
-  .ui.ui.ui.grid.container {
-    width: calc(1127px + 2rem);
-  }
-
-  .ui.ui.ui.relaxed.grid.container {
-    width: calc(1127px + 3rem);
-  }
-
-  .ui.ui.ui.very.relaxed.grid.container {
-    width: calc(1127px + 5rem);
-  }
-}
-
-/*******************************
-             Types
-*******************************/
-
-/* Text Container */
-
-.ui.text.container {
-  font-family: var(--fonts-regular);
-  max-width: 700px;
-  line-height: 1.5;
-  font-size: 1.14285714rem;
-}
-
-/* Fluid */
-
-.ui.fluid.container {
-  width: 100%;
-}
-
-/*******************************
-           Variations
-*******************************/
-
-.ui[class*="left aligned"].container {
-  text-align: left;
-}
-
-.ui[class*="center aligned"].container {
-  text-align: center;
-}
-
-.ui[class*="right aligned"].container {
-  text-align: right;
-}
-
-.ui.justified.container {
-  text-align: justify;
-  -webkit-hyphens: auto;
-  hyphens: auto;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
 /*******************************
          Site Overrides
 *******************************/
@@ -3227,9 +2357,7 @@
   background: rgba(0, 0, 0, 0.85);
   opacity: 0;
   line-height: 1;
-  -webkit-animation-fill-mode: both;
   animation-fill-mode: both;
-  -webkit-animation-duration: 0.5s;
   animation-duration: 0.5s;
   transition: background-color 0.5s linear;
   flex-direction: column;
@@ -3458,44 +2586,25 @@ body.dimmable > .dimmer {
 }
 
 .ui[class*="center dimmer"].transition[class*="fade up"].in {
-  -webkit-animation-name: fadeInUpCenter;
   animation-name: fadeInUpCenter;
 }
 
 .ui[class*="center dimmer"].transition[class*="fade down"].in {
-  -webkit-animation-name: fadeInDownCenter;
   animation-name: fadeInDownCenter;
 }
 
 .ui[class*="center dimmer"].transition[class*="fade up"].out {
-  -webkit-animation-name: fadeOutUpCenter;
   animation-name: fadeOutUpCenter;
 }
 
 .ui[class*="center dimmer"].transition[class*="fade down"].out {
-  -webkit-animation-name: fadeOutDownCenter;
   animation-name: fadeOutDownCenter;
 }
 
 .ui[class*="center dimmer"].bounce.transition {
-  -webkit-animation-name: bounceCenter;
   animation-name: bounceCenter;
 }
 
-@-webkit-keyframes fadeInUpCenter {
-  0% {
-    opacity: 0;
-    transform: translateY(-40%);
-    -webkit-transform: translateY(calc(-40% - 0.5px));
-  }
-
-  100% {
-    opacity: 1;
-    transform: translateY(-50%);
-    -webkit-transform: translateY(calc(-50% - 0.5px));
-  }
-}
-
 @keyframes fadeInUpCenter {
   0% {
     opacity: 0;
@@ -3510,20 +2619,6 @@ body.dimmable > .dimmer {
   }
 }
 
-@-webkit-keyframes fadeInDownCenter {
-  0% {
-    opacity: 0;
-    transform: translateY(-60%);
-    -webkit-transform: translateY(calc(-60% - 0.5px));
-  }
-
-  100% {
-    opacity: 1;
-    transform: translateY(-50%);
-    -webkit-transform: translateY(calc(-50% - 0.5px));
-  }
-}
-
 @keyframes fadeInDownCenter {
   0% {
     opacity: 0;
@@ -3538,20 +2633,6 @@ body.dimmable > .dimmer {
   }
 }
 
-@-webkit-keyframes fadeOutUpCenter {
-  0% {
-    opacity: 1;
-    transform: translateY(-50%);
-    -webkit-transform: translateY(calc(-50% - 0.5px));
-  }
-
-  100% {
-    opacity: 0;
-    transform: translateY(-45%);
-    -webkit-transform: translateY(calc(-45% - 0.5px));
-  }
-}
-
 @keyframes fadeOutUpCenter {
   0% {
     opacity: 1;
@@ -3566,20 +2647,6 @@ body.dimmable > .dimmer {
   }
 }
 
-@-webkit-keyframes fadeOutDownCenter {
-  0% {
-    opacity: 1;
-    transform: translateY(-50%);
-    -webkit-transform: translateY(calc(-50% - 0.5px));
-  }
-
-  100% {
-    opacity: 0;
-    transform: translateY(-55%);
-    -webkit-transform: translateY(calc(-55% - 0.5px));
-  }
-}
-
 @keyframes fadeOutDownCenter {
   0% {
     opacity: 1;
@@ -3594,21 +2661,6 @@ body.dimmable > .dimmer {
   }
 }
 
-@-webkit-keyframes bounceCenter {
-  0%, 20%, 50%, 80%, 100% {
-    transform: translateY(-50%);
-    -webkit-transform: translateY(calc(-50% - 0.5px));
-  }
-
-  40% {
-    transform: translateY(calc(-50% - 30px));
-  }
-
-  60% {
-    transform: translateY(calc(-50% - 15px));
-  }
-}
-
 @keyframes bounceCenter {
   0%, 20%, 50%, 80%, 100% {
     transform: translateY(-50%);
@@ -3672,7 +2724,6 @@ body.dimmable > .dimmer {
   display: none;
   outline: none;
   top: 100%;
-  min-width: -webkit-max-content;
   min-width: -moz-max-content;
   min-width: max-content;
   margin: 0;
@@ -4068,7 +3119,6 @@ select.ui.dropdown {
 .ui.selection.dropdown .menu {
   overflow-x: hidden;
   overflow-y: auto;
-  -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
   -webkit-overflow-scrolling: touch;
   border-top-width: 0 !important;
@@ -4280,7 +3330,6 @@ select.ui.dropdown {
 @supports (-webkit-touch-callout: none) or (-webkit-overflow-scrolling: touch) or (-moz-appearance:none) {
 @media (-moz-touch-enabled), (pointer: coarse) {
     .ui.dropdown .scrollhint.menu:not(.hidden):before {
-      -webkit-animation: scrollhint 2s ease 2;
       animation: scrollhint 2s ease 2;
       content: '';
       z-index: 15;
@@ -4301,18 +3350,6 @@ select.ui.dropdown {
       border-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0)) 1 100%;
     }
 
-@-webkit-keyframes scrollhint {
-      0% {
-        opacity: 1;
-        top: 100%;
-      }
-
-      100% {
-        opacity: 0;
-        top: 0;
-      }
-}
-
 @keyframes scrollhint {
       0% {
         opacity: 1;
@@ -4414,7 +3451,6 @@ select.ui.dropdown {
 .ui.search.dropdown .menu {
   overflow-x: hidden;
   overflow-y: auto;
-  -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
   -webkit-overflow-scrolling: touch;
 }
@@ -4682,7 +3718,6 @@ select.ui.dropdown {
   margin: -0.64285714em 0 0 -0.64285714em;
   width: 1.28571429em;
   height: 1.28571429em;
-  -webkit-animation: loader 0.6s infinite linear;
   animation: loader 0.6s infinite linear;
   border: 0.2em solid #767676;
   border-radius: 500rem;
@@ -5072,7 +4107,6 @@ select.ui.dropdown {
 .ui.scrolling.dropdown .menu {
   overflow-x: hidden;
   overflow-y: auto;
-  -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
   -webkit-overflow-scrolling: touch;
   min-width: 100% !important;
@@ -5564,7 +4598,6 @@ select.ui.dropdown {
   line-height: 1;
   height: 1em;
   width: 1.23em;
-  -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
   font-weight: normal;
   font-style: normal;
@@ -7052,7 +6085,6 @@ select.ui.dropdown {
   margin: -1.5em 0 0 -1.5em;
   width: 3em;
   height: 3em;
-  -webkit-animation: loader 0.6s infinite linear;
   animation: loader 0.6s infinite linear;
   border: 0.2em solid #767676;
   border-radius: 500rem;
@@ -7445,5580 +6477,6 @@ select.ui.dropdown {
 /*******************************
          Site Overrides
 *******************************/
-/*!
- * # Fomantic-UI - Grid
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            Standard
-*******************************/
-
-.ui.grid {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  align-items: stretch;
-  padding: 0;
-}
-
-/*----------------------
-      Remove Gutters
------------------------*/
-
-.ui.grid {
-  margin-top: -1rem;
-  margin-bottom: -1rem;
-  margin-left: -1rem;
-  margin-right: -1rem;
-}
-
-.ui.relaxed.grid {
-  margin-left: -1.5rem;
-  margin-right: -1.5rem;
-}
-
-.ui[class*="very relaxed"].grid {
-  margin-left: -2.5rem;
-  margin-right: -2.5rem;
-}
-
-/* Preserve Rows Spacing on Consecutive Grids */
-
-.ui.grid + .grid {
-  margin-top: 1rem;
-}
-
-/*-------------------
-       Columns
---------------------*/
-
-/* Standard 16 column */
-
-.ui.grid > .column:not(.row),
-.ui.grid > .row > .column {
-  position: relative;
-  display: inline-block;
-  width: 6.25%;
-  padding-left: 1rem;
-  padding-right: 1rem;
-  vertical-align: top;
-}
-
-.ui.grid > * {
-  padding-left: 1rem;
-  padding-right: 1rem;
-}
-
-/*-------------------
-        Rows
---------------------*/
-
-.ui.grid > .row {
-  position: relative;
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  justify-content: inherit;
-  align-items: stretch;
-  width: 100% !important;
-  padding: 0;
-  padding-top: 1rem;
-  padding-bottom: 1rem;
-}
-
-/*-------------------
-       Columns
---------------------*/
-
-/* Vertical padding when no rows */
-
-.ui.grid > .column:not(.row) {
-  padding-top: 1rem;
-  padding-bottom: 1rem;
-}
-
-.ui.grid > .row > .column {
-  margin-top: 0;
-  margin-bottom: 0;
-}
-
-/*-------------------
-      Content
---------------------*/
-
-.ui.grid > .row > img,
-.ui.grid > .row > .column > img {
-  max-width: 100%;
-}
-
-/*-------------------
-    Loose Coupling
---------------------*/
-
-/* Collapse Margin on Consecutive Grid */
-
-.ui.grid > .ui.grid:first-child {
-  margin-top: 0;
-}
-
-.ui.grid > .ui.grid:last-child {
-  margin-bottom: 0;
-}
-
-/* Segment inside Aligned Grid */
-
-.ui.grid .aligned.row > .column > .segment:not(.compact):not(.attached),
-.ui.aligned.grid .column > .segment:not(.compact):not(.attached) {
-  width: 100%;
-}
-
-/* Align Dividers with Gutter */
-
-.ui.grid .row + .ui.divider {
-  flex-grow: 1;
-  margin: 1rem 1rem;
-}
-
-.ui.grid .column + .ui.vertical.divider {
-  height: calc(50% - 1rem);
-}
-
-/* Remove Border on Last Horizontal Segment */
-
-.ui.grid > .row > .column:last-child > .horizontal.segment,
-.ui.grid > .column:last-child > .horizontal.segment {
-  box-shadow: none;
-}
-
-/*******************************
-           Variations
-*******************************/
-
-/*-----------------------
-         Page Grid
-  -------------------------*/
-
-@media only screen and (max-width: 767.98px) {
-  .ui.page.grid {
-    width: auto;
-    padding-left: 0;
-    padding-right: 0;
-    margin-left: 0;
-    margin-right: 0;
-  }
-}
-
-@media only screen and (min-width: 768px) and (max-width: 991.98px) {
-  .ui.page.grid {
-    width: auto;
-    margin-left: 0;
-    margin-right: 0;
-    padding-left: 2em;
-    padding-right: 2em;
-  }
-}
-
-@media only screen and (min-width: 992px) and (max-width: 1199.98px) {
-  .ui.page.grid {
-    width: auto;
-    margin-left: 0;
-    margin-right: 0;
-    padding-left: 3%;
-    padding-right: 3%;
-  }
-}
-
-@media only screen and (min-width: 1200px) and (max-width: 1919.98px) {
-  .ui.page.grid {
-    width: auto;
-    margin-left: 0;
-    margin-right: 0;
-    padding-left: 15%;
-    padding-right: 15%;
-  }
-}
-
-@media only screen and (min-width: 1920px) {
-  .ui.page.grid {
-    width: auto;
-    margin-left: 0;
-    margin-right: 0;
-    padding-left: 23%;
-    padding-right: 23%;
-  }
-}
-
-/*-------------------
-     Column Count
---------------------*/
-
-/* Assume full width with one column */
-
-.ui.grid > .column:only-child,
-.ui.grid > .row > .column:only-child {
-  width: 100%;
-}
-
-/* Grid Based */
-
-.ui[class*="one column"].grid > .row > .column,
-.ui[class*="one column"].grid > .column:not(.row) {
-  width: 100%;
-}
-
-.ui[class*="two column"].grid > .row > .column,
-.ui[class*="two column"].grid > .column:not(.row) {
-  width: 50%;
-}
-
-.ui[class*="three column"].grid > .row > .column,
-.ui[class*="three column"].grid > .column:not(.row) {
-  width: 33.33333333%;
-}
-
-.ui[class*="four column"].grid > .row > .column,
-.ui[class*="four column"].grid > .column:not(.row) {
-  width: 25%;
-}
-
-.ui[class*="five column"].grid > .row > .column,
-.ui[class*="five column"].grid > .column:not(.row) {
-  width: 20%;
-}
-
-.ui[class*="six column"].grid > .row > .column,
-.ui[class*="six column"].grid > .column:not(.row) {
-  width: 16.66666667%;
-}
-
-.ui[class*="seven column"].grid > .row > .column,
-.ui[class*="seven column"].grid > .column:not(.row) {
-  width: 14.28571429%;
-}
-
-.ui[class*="eight column"].grid > .row > .column,
-.ui[class*="eight column"].grid > .column:not(.row) {
-  width: 12.5%;
-}
-
-.ui[class*="nine column"].grid > .row > .column,
-.ui[class*="nine column"].grid > .column:not(.row) {
-  width: 11.11111111%;
-}
-
-.ui[class*="ten column"].grid > .row > .column,
-.ui[class*="ten column"].grid > .column:not(.row) {
-  width: 10%;
-}
-
-.ui[class*="eleven column"].grid > .row > .column,
-.ui[class*="eleven column"].grid > .column:not(.row) {
-  width: 9.09090909%;
-}
-
-.ui[class*="twelve column"].grid > .row > .column,
-.ui[class*="twelve column"].grid > .column:not(.row) {
-  width: 8.33333333%;
-}
-
-.ui[class*="thirteen column"].grid > .row > .column,
-.ui[class*="thirteen column"].grid > .column:not(.row) {
-  width: 7.69230769%;
-}
-
-.ui[class*="fourteen column"].grid > .row > .column,
-.ui[class*="fourteen column"].grid > .column:not(.row) {
-  width: 7.14285714%;
-}
-
-.ui[class*="fifteen column"].grid > .row > .column,
-.ui[class*="fifteen column"].grid > .column:not(.row) {
-  width: 6.66666667%;
-}
-
-.ui[class*="sixteen column"].grid > .row > .column,
-.ui[class*="sixteen column"].grid > .column:not(.row) {
-  width: 6.25%;
-}
-
-/* Row Based Overrides */
-
-.ui.grid > [class*="one column"].row > .column {
-  width: 100% !important;
-}
-
-.ui.grid > [class*="two column"].row > .column {
-  width: 50% !important;
-}
-
-.ui.grid > [class*="three column"].row > .column {
-  width: 33.33333333% !important;
-}
-
-.ui.grid > [class*="four column"].row > .column {
-  width: 25% !important;
-}
-
-.ui.grid > [class*="five column"].row > .column {
-  width: 20% !important;
-}
-
-.ui.grid > [class*="six column"].row > .column {
-  width: 16.66666667% !important;
-}
-
-.ui.grid > [class*="seven column"].row > .column {
-  width: 14.28571429% !important;
-}
-
-.ui.grid > [class*="eight column"].row > .column {
-  width: 12.5% !important;
-}
-
-.ui.grid > [class*="nine column"].row > .column {
-  width: 11.11111111% !important;
-}
-
-.ui.grid > [class*="ten column"].row > .column {
-  width: 10% !important;
-}
-
-.ui.grid > [class*="eleven column"].row > .column {
-  width: 9.09090909% !important;
-}
-
-.ui.grid > [class*="twelve column"].row > .column {
-  width: 8.33333333% !important;
-}
-
-.ui.grid > [class*="thirteen column"].row > .column {
-  width: 7.69230769% !important;
-}
-
-.ui.grid > [class*="fourteen column"].row > .column {
-  width: 7.14285714% !important;
-}
-
-.ui.grid > [class*="fifteen column"].row > .column {
-  width: 6.66666667% !important;
-}
-
-.ui.grid > [class*="sixteen column"].row > .column {
-  width: 6.25% !important;
-}
-
-/* Celled Page */
-
-.ui.celled.page.grid {
-  box-shadow: none;
-}
-
-/*-------------------
-    Column Width
---------------------*/
-
-/* Sizing Combinations */
-
-.ui.grid > .row > [class*="one wide"].column,
-.ui.grid > .column.row > [class*="one wide"].column,
-.ui.grid > [class*="one wide"].column,
-.ui.column.grid > [class*="one wide"].column {
-  width: 6.25% !important;
-}
-
-.ui.grid > .row > [class*="two wide"].column,
-.ui.grid > .column.row > [class*="two wide"].column,
-.ui.grid > [class*="two wide"].column,
-.ui.column.grid > [class*="two wide"].column {
-  width: 12.5% !important;
-}
-
-.ui.grid > .row > [class*="three wide"].column,
-.ui.grid > .column.row > [class*="three wide"].column,
-.ui.grid > [class*="three wide"].column,
-.ui.column.grid > [class*="three wide"].column {
-  width: 18.75% !important;
-}
-
-.ui.grid > .row > [class*="four wide"].column,
-.ui.grid > .column.row > [class*="four wide"].column,
-.ui.grid > [class*="four wide"].column,
-.ui.column.grid > [class*="four wide"].column {
-  width: 25% !important;
-}
-
-.ui.grid > .row > [class*="five wide"].column,
-.ui.grid > .column.row > [class*="five wide"].column,
-.ui.grid > [class*="five wide"].column,
-.ui.column.grid > [class*="five wide"].column {
-  width: 31.25% !important;
-}
-
-.ui.grid > .row > [class*="six wide"].column,
-.ui.grid > .column.row > [class*="six wide"].column,
-.ui.grid > [class*="six wide"].column,
-.ui.column.grid > [class*="six wide"].column {
-  width: 37.5% !important;
-}
-
-.ui.grid > .row > [class*="seven wide"].column,
-.ui.grid > .column.row > [class*="seven wide"].column,
-.ui.grid > [class*="seven wide"].column,
-.ui.column.grid > [class*="seven wide"].column {
-  width: 43.75% !important;
-}
-
-.ui.grid > .row > [class*="eight wide"].column,
-.ui.grid > .column.row > [class*="eight wide"].column,
-.ui.grid > [class*="eight wide"].column,
-.ui.column.grid > [class*="eight wide"].column {
-  width: 50% !important;
-}
-
-.ui.grid > .row > [class*="nine wide"].column,
-.ui.grid > .column.row > [class*="nine wide"].column,
-.ui.grid > [class*="nine wide"].column,
-.ui.column.grid > [class*="nine wide"].column {
-  width: 56.25% !important;
-}
-
-.ui.grid > .row > [class*="ten wide"].column,
-.ui.grid > .column.row > [class*="ten wide"].column,
-.ui.grid > [class*="ten wide"].column,
-.ui.column.grid > [class*="ten wide"].column {
-  width: 62.5% !important;
-}
-
-.ui.grid > .row > [class*="eleven wide"].column,
-.ui.grid > .column.row > [class*="eleven wide"].column,
-.ui.grid > [class*="eleven wide"].column,
-.ui.column.grid > [class*="eleven wide"].column {
-  width: 68.75% !important;
-}
-
-.ui.grid > .row > [class*="twelve wide"].column,
-.ui.grid > .column.row > [class*="twelve wide"].column,
-.ui.grid > [class*="twelve wide"].column,
-.ui.column.grid > [class*="twelve wide"].column {
-  width: 75% !important;
-}
-
-.ui.grid > .row > [class*="thirteen wide"].column,
-.ui.grid > .column.row > [class*="thirteen wide"].column,
-.ui.grid > [class*="thirteen wide"].column,
-.ui.column.grid > [class*="thirteen wide"].column {
-  width: 81.25% !important;
-}
-
-.ui.grid > .row > [class*="fourteen wide"].column,
-.ui.grid > .column.row > [class*="fourteen wide"].column,
-.ui.grid > [class*="fourteen wide"].column,
-.ui.column.grid > [class*="fourteen wide"].column {
-  width: 87.5% !important;
-}
-
-.ui.grid > .row > [class*="fifteen wide"].column,
-.ui.grid > .column.row > [class*="fifteen wide"].column,
-.ui.grid > [class*="fifteen wide"].column,
-.ui.column.grid > [class*="fifteen wide"].column {
-  width: 93.75% !important;
-}
-
-.ui.grid > .row > [class*="sixteen wide"].column,
-.ui.grid > .column.row > [class*="sixteen wide"].column,
-.ui.grid > [class*="sixteen wide"].column,
-.ui.column.grid > [class*="sixteen wide"].column {
-  width: 100% !important;
-}
-
-/*----------------------
-    Width per Device
------------------------*/
-
-/* Mobile Sizing Combinations */
-
-@media only screen and (min-width: 320px) and (max-width: 767.98px) {
-  .ui.grid > .row > [class*="one wide mobile"].column,
-  .ui.grid > .column.row > [class*="one wide mobile"].column,
-  .ui.grid > [class*="one wide mobile"].column,
-  .ui.column.grid > [class*="one wide mobile"].column {
-    width: 6.25% !important;
-  }
-
-  .ui.grid > .row > [class*="two wide mobile"].column,
-  .ui.grid > .column.row > [class*="two wide mobile"].column,
-  .ui.grid > [class*="two wide mobile"].column,
-  .ui.column.grid > [class*="two wide mobile"].column {
-    width: 12.5% !important;
-  }
-
-  .ui.grid > .row > [class*="three wide mobile"].column,
-  .ui.grid > .column.row > [class*="three wide mobile"].column,
-  .ui.grid > [class*="three wide mobile"].column,
-  .ui.column.grid > [class*="three wide mobile"].column {
-    width: 18.75% !important;
-  }
-
-  .ui.grid > .row > [class*="four wide mobile"].column,
-  .ui.grid > .column.row > [class*="four wide mobile"].column,
-  .ui.grid > [class*="four wide mobile"].column,
-  .ui.column.grid > [class*="four wide mobile"].column {
-    width: 25% !important;
-  }
-
-  .ui.grid > .row > [class*="five wide mobile"].column,
-  .ui.grid > .column.row > [class*="five wide mobile"].column,
-  .ui.grid > [class*="five wide mobile"].column,
-  .ui.column.grid > [class*="five wide mobile"].column {
-    width: 31.25% !important;
-  }
-
-  .ui.grid > .row > [class*="six wide mobile"].column,
-  .ui.grid > .column.row > [class*="six wide mobile"].column,
-  .ui.grid > [class*="six wide mobile"].column,
-  .ui.column.grid > [class*="six wide mobile"].column {
-    width: 37.5% !important;
-  }
-
-  .ui.grid > .row > [class*="seven wide mobile"].column,
-  .ui.grid > .column.row > [class*="seven wide mobile"].column,
-  .ui.grid > [class*="seven wide mobile"].column,
-  .ui.column.grid > [class*="seven wide mobile"].column {
-    width: 43.75% !important;
-  }
-
-  .ui.grid > .row > [class*="eight wide mobile"].column,
-  .ui.grid > .column.row > [class*="eight wide mobile"].column,
-  .ui.grid > [class*="eight wide mobile"].column,
-  .ui.column.grid > [class*="eight wide mobile"].column {
-    width: 50% !important;
-  }
-
-  .ui.grid > .row > [class*="nine wide mobile"].column,
-  .ui.grid > .column.row > [class*="nine wide mobile"].column,
-  .ui.grid > [class*="nine wide mobile"].column,
-  .ui.column.grid > [class*="nine wide mobile"].column {
-    width: 56.25% !important;
-  }
-
-  .ui.grid > .row > [class*="ten wide mobile"].column,
-  .ui.grid > .column.row > [class*="ten wide mobile"].column,
-  .ui.grid > [class*="ten wide mobile"].column,
-  .ui.column.grid > [class*="ten wide mobile"].column {
-    width: 62.5% !important;
-  }
-
-  .ui.grid > .row > [class*="eleven wide mobile"].column,
-  .ui.grid > .column.row > [class*="eleven wide mobile"].column,
-  .ui.grid > [class*="eleven wide mobile"].column,
-  .ui.column.grid > [class*="eleven wide mobile"].column {
-    width: 68.75% !important;
-  }
-
-  .ui.grid > .row > [class*="twelve wide mobile"].column,
-  .ui.grid > .column.row > [class*="twelve wide mobile"].column,
-  .ui.grid > [class*="twelve wide mobile"].column,
-  .ui.column.grid > [class*="twelve wide mobile"].column {
-    width: 75% !important;
-  }
-
-  .ui.grid > .row > [class*="thirteen wide mobile"].column,
-  .ui.grid > .column.row > [class*="thirteen wide mobile"].column,
-  .ui.grid > [class*="thirteen wide mobile"].column,
-  .ui.column.grid > [class*="thirteen wide mobile"].column {
-    width: 81.25% !important;
-  }
-
-  .ui.grid > .row > [class*="fourteen wide mobile"].column,
-  .ui.grid > .column.row > [class*="fourteen wide mobile"].column,
-  .ui.grid > [class*="fourteen wide mobile"].column,
-  .ui.column.grid > [class*="fourteen wide mobile"].column {
-    width: 87.5% !important;
-  }
-
-  .ui.grid > .row > [class*="fifteen wide mobile"].column,
-  .ui.grid > .column.row > [class*="fifteen wide mobile"].column,
-  .ui.grid > [class*="fifteen wide mobile"].column,
-  .ui.column.grid > [class*="fifteen wide mobile"].column {
-    width: 93.75% !important;
-  }
-
-  .ui.grid > .row > [class*="sixteen wide mobile"].column,
-  .ui.grid > .column.row > [class*="sixteen wide mobile"].column,
-  .ui.grid > [class*="sixteen wide mobile"].column,
-  .ui.column.grid > [class*="sixteen wide mobile"].column {
-    width: 100% !important;
-  }
-}
-
-/* Tablet Sizing Combinations */
-
-@media only screen and (min-width: 768px) and (max-width: 991.98px) {
-  .ui.grid > .row > [class*="one wide tablet"].column,
-  .ui.grid > .column.row > [class*="one wide tablet"].column,
-  .ui.grid > [class*="one wide tablet"].column,
-  .ui.column.grid > [class*="one wide tablet"].column {
-    width: 6.25% !important;
-  }
-
-  .ui.grid > .row > [class*="two wide tablet"].column,
-  .ui.grid > .column.row > [class*="two wide tablet"].column,
-  .ui.grid > [class*="two wide tablet"].column,
-  .ui.column.grid > [class*="two wide tablet"].column {
-    width: 12.5% !important;
-  }
-
-  .ui.grid > .row > [class*="three wide tablet"].column,
-  .ui.grid > .column.row > [class*="three wide tablet"].column,
-  .ui.grid > [class*="three wide tablet"].column,
-  .ui.column.grid > [class*="three wide tablet"].column {
-    width: 18.75% !important;
-  }
-
-  .ui.grid > .row > [class*="four wide tablet"].column,
-  .ui.grid > .column.row > [class*="four wide tablet"].column,
-  .ui.grid > [class*="four wide tablet"].column,
-  .ui.column.grid > [class*="four wide tablet"].column {
-    width: 25% !important;
-  }
-
-  .ui.grid > .row > [class*="five wide tablet"].column,
-  .ui.grid > .column.row > [class*="five wide tablet"].column,
-  .ui.grid > [class*="five wide tablet"].column,
-  .ui.column.grid > [class*="five wide tablet"].column {
-    width: 31.25% !important;
-  }
-
-  .ui.grid > .row > [class*="six wide tablet"].column,
-  .ui.grid > .column.row > [class*="six wide tablet"].column,
-  .ui.grid > [class*="six wide tablet"].column,
-  .ui.column.grid > [class*="six wide tablet"].column {
-    width: 37.5% !important;
-  }
-
-  .ui.grid > .row > [class*="seven wide tablet"].column,
-  .ui.grid > .column.row > [class*="seven wide tablet"].column,
-  .ui.grid > [class*="seven wide tablet"].column,
-  .ui.column.grid > [class*="seven wide tablet"].column {
-    width: 43.75% !important;
-  }
-
-  .ui.grid > .row > [class*="eight wide tablet"].column,
-  .ui.grid > .column.row > [class*="eight wide tablet"].column,
-  .ui.grid > [class*="eight wide tablet"].column,
-  .ui.column.grid > [class*="eight wide tablet"].column {
-    width: 50% !important;
-  }
-
-  .ui.grid > .row > [class*="nine wide tablet"].column,
-  .ui.grid > .column.row > [class*="nine wide tablet"].column,
-  .ui.grid > [class*="nine wide tablet"].column,
-  .ui.column.grid > [class*="nine wide tablet"].column {
-    width: 56.25% !important;
-  }
-
-  .ui.grid > .row > [class*="ten wide tablet"].column,
-  .ui.grid > .column.row > [class*="ten wide tablet"].column,
-  .ui.grid > [class*="ten wide tablet"].column,
-  .ui.column.grid > [class*="ten wide tablet"].column {
-    width: 62.5% !important;
-  }
-
-  .ui.grid > .row > [class*="eleven wide tablet"].column,
-  .ui.grid > .column.row > [class*="eleven wide tablet"].column,
-  .ui.grid > [class*="eleven wide tablet"].column,
-  .ui.column.grid > [class*="eleven wide tablet"].column {
-    width: 68.75% !important;
-  }
-
-  .ui.grid > .row > [class*="twelve wide tablet"].column,
-  .ui.grid > .column.row > [class*="twelve wide tablet"].column,
-  .ui.grid > [class*="twelve wide tablet"].column,
-  .ui.column.grid > [class*="twelve wide tablet"].column {
-    width: 75% !important;
-  }
-
-  .ui.grid > .row > [class*="thirteen wide tablet"].column,
-  .ui.grid > .column.row > [class*="thirteen wide tablet"].column,
-  .ui.grid > [class*="thirteen wide tablet"].column,
-  .ui.column.grid > [class*="thirteen wide tablet"].column {
-    width: 81.25% !important;
-  }
-
-  .ui.grid > .row > [class*="fourteen wide tablet"].column,
-  .ui.grid > .column.row > [class*="fourteen wide tablet"].column,
-  .ui.grid > [class*="fourteen wide tablet"].column,
-  .ui.column.grid > [class*="fourteen wide tablet"].column {
-    width: 87.5% !important;
-  }
-
-  .ui.grid > .row > [class*="fifteen wide tablet"].column,
-  .ui.grid > .column.row > [class*="fifteen wide tablet"].column,
-  .ui.grid > [class*="fifteen wide tablet"].column,
-  .ui.column.grid > [class*="fifteen wide tablet"].column {
-    width: 93.75% !important;
-  }
-
-  .ui.grid > .row > [class*="sixteen wide tablet"].column,
-  .ui.grid > .column.row > [class*="sixteen wide tablet"].column,
-  .ui.grid > [class*="sixteen wide tablet"].column,
-  .ui.column.grid > [class*="sixteen wide tablet"].column {
-    width: 100% !important;
-  }
-}
-
-/* Computer/Desktop Sizing Combinations */
-
-@media only screen and (min-width: 992px) {
-  .ui.grid > .row > [class*="one wide computer"].column,
-  .ui.grid > .column.row > [class*="one wide computer"].column,
-  .ui.grid > [class*="one wide computer"].column,
-  .ui.column.grid > [class*="one wide computer"].column {
-    width: 6.25% !important;
-  }
-
-  .ui.grid > .row > [class*="two wide computer"].column,
-  .ui.grid > .column.row > [class*="two wide computer"].column,
-  .ui.grid > [class*="two wide computer"].column,
-  .ui.column.grid > [class*="two wide computer"].column {
-    width: 12.5% !important;
-  }
-
-  .ui.grid > .row > [class*="three wide computer"].column,
-  .ui.grid > .column.row > [class*="three wide computer"].column,
-  .ui.grid > [class*="three wide computer"].column,
-  .ui.column.grid > [class*="three wide computer"].column {
-    width: 18.75% !important;
-  }
-
-  .ui.grid > .row > [class*="four wide computer"].column,
-  .ui.grid > .column.row > [class*="four wide computer"].column,
-  .ui.grid > [class*="four wide computer"].column,
-  .ui.column.grid > [class*="four wide computer"].column {
-    width: 25% !important;
-  }
-
-  .ui.grid > .row > [class*="five wide computer"].column,
-  .ui.grid > .column.row > [class*="five wide computer"].column,
-  .ui.grid > [class*="five wide computer"].column,
-  .ui.column.grid > [class*="five wide computer"].column {
-    width: 31.25% !important;
-  }
-
-  .ui.grid > .row > [class*="six wide computer"].column,
-  .ui.grid > .column.row > [class*="six wide computer"].column,
-  .ui.grid > [class*="six wide computer"].column,
-  .ui.column.grid > [class*="six wide computer"].column {
-    width: 37.5% !important;
-  }
-
-  .ui.grid > .row > [class*="seven wide computer"].column,
-  .ui.grid > .column.row > [class*="seven wide computer"].column,
-  .ui.grid > [class*="seven wide computer"].column,
-  .ui.column.grid > [class*="seven wide computer"].column {
-    width: 43.75% !important;
-  }
-
-  .ui.grid > .row > [class*="eight wide computer"].column,
-  .ui.grid > .column.row > [class*="eight wide computer"].column,
-  .ui.grid > [class*="eight wide computer"].column,
-  .ui.column.grid > [class*="eight wide computer"].column {
-    width: 50% !important;
-  }
-
-  .ui.grid > .row > [class*="nine wide computer"].column,
-  .ui.grid > .column.row > [class*="nine wide computer"].column,
-  .ui.grid > [class*="nine wide computer"].column,
-  .ui.column.grid > [class*="nine wide computer"].column {
-    width: 56.25% !important;
-  }
-
-  .ui.grid > .row > [class*="ten wide computer"].column,
-  .ui.grid > .column.row > [class*="ten wide computer"].column,
-  .ui.grid > [class*="ten wide computer"].column,
-  .ui.column.grid > [class*="ten wide computer"].column {
-    width: 62.5% !important;
-  }
-
-  .ui.grid > .row > [class*="eleven wide computer"].column,
-  .ui.grid > .column.row > [class*="eleven wide computer"].column,
-  .ui.grid > [class*="eleven wide computer"].column,
-  .ui.column.grid > [class*="eleven wide computer"].column {
-    width: 68.75% !important;
-  }
-
-  .ui.grid > .row > [class*="twelve wide computer"].column,
-  .ui.grid > .column.row > [class*="twelve wide computer"].column,
-  .ui.grid > [class*="twelve wide computer"].column,
-  .ui.column.grid > [class*="twelve wide computer"].column {
-    width: 75% !important;
-  }
-
-  .ui.grid > .row > [class*="thirteen wide computer"].column,
-  .ui.grid > .column.row > [class*="thirteen wide computer"].column,
-  .ui.grid > [class*="thirteen wide computer"].column,
-  .ui.column.grid > [class*="thirteen wide computer"].column {
-    width: 81.25% !important;
-  }
-
-  .ui.grid > .row > [class*="fourteen wide computer"].column,
-  .ui.grid > .column.row > [class*="fourteen wide computer"].column,
-  .ui.grid > [class*="fourteen wide computer"].column,
-  .ui.column.grid > [class*="fourteen wide computer"].column {
-    width: 87.5% !important;
-  }
-
-  .ui.grid > .row > [class*="fifteen wide computer"].column,
-  .ui.grid > .column.row > [class*="fifteen wide computer"].column,
-  .ui.grid > [class*="fifteen wide computer"].column,
-  .ui.column.grid > [class*="fifteen wide computer"].column {
-    width: 93.75% !important;
-  }
-
-  .ui.grid > .row > [class*="sixteen wide computer"].column,
-  .ui.grid > .column.row > [class*="sixteen wide computer"].column,
-  .ui.grid > [class*="sixteen wide computer"].column,
-  .ui.column.grid > [class*="sixteen wide computer"].column {
-    width: 100% !important;
-  }
-}
-
-/* Large Monitor Sizing Combinations */
-
-@media only screen and (min-width: 1200px) and (max-width: 1919.98px) {
-  .ui.grid > .row > [class*="one wide large screen"].column,
-  .ui.grid > .column.row > [class*="one wide large screen"].column,
-  .ui.grid > [class*="one wide large screen"].column,
-  .ui.column.grid > [class*="one wide large screen"].column {
-    width: 6.25% !important;
-  }
-
-  .ui.grid > .row > [class*="two wide large screen"].column,
-  .ui.grid > .column.row > [class*="two wide large screen"].column,
-  .ui.grid > [class*="two wide large screen"].column,
-  .ui.column.grid > [class*="two wide large screen"].column {
-    width: 12.5% !important;
-  }
-
-  .ui.grid > .row > [class*="three wide large screen"].column,
-  .ui.grid > .column.row > [class*="three wide large screen"].column,
-  .ui.grid > [class*="three wide large screen"].column,
-  .ui.column.grid > [class*="three wide large screen"].column {
-    width: 18.75% !important;
-  }
-
-  .ui.grid > .row > [class*="four wide large screen"].column,
-  .ui.grid > .column.row > [class*="four wide large screen"].column,
-  .ui.grid > [class*="four wide large screen"].column,
-  .ui.column.grid > [class*="four wide large screen"].column {
-    width: 25% !important;
-  }
-
-  .ui.grid > .row > [class*="five wide large screen"].column,
-  .ui.grid > .column.row > [class*="five wide large screen"].column,
-  .ui.grid > [class*="five wide large screen"].column,
-  .ui.column.grid > [class*="five wide large screen"].column {
-    width: 31.25% !important;
-  }
-
-  .ui.grid > .row > [class*="six wide large screen"].column,
-  .ui.grid > .column.row > [class*="six wide large screen"].column,
-  .ui.grid > [class*="six wide large screen"].column,
-  .ui.column.grid > [class*="six wide large screen"].column {
-    width: 37.5% !important;
-  }
-
-  .ui.grid > .row > [class*="seven wide large screen"].column,
-  .ui.grid > .column.row > [class*="seven wide large screen"].column,
-  .ui.grid > [class*="seven wide large screen"].column,
-  .ui.column.grid > [class*="seven wide large screen"].column {
-    width: 43.75% !important;
-  }
-
-  .ui.grid > .row > [class*="eight wide large screen"].column,
-  .ui.grid > .column.row > [class*="eight wide large screen"].column,
-  .ui.grid > [class*="eight wide large screen"].column,
-  .ui.column.grid > [class*="eight wide large screen"].column {
-    width: 50% !important;
-  }
-
-  .ui.grid > .row > [class*="nine wide large screen"].column,
-  .ui.grid > .column.row > [class*="nine wide large screen"].column,
-  .ui.grid > [class*="nine wide large screen"].column,
-  .ui.column.grid > [class*="nine wide large screen"].column {
-    width: 56.25% !important;
-  }
-
-  .ui.grid > .row > [class*="ten wide large screen"].column,
-  .ui.grid > .column.row > [class*="ten wide large screen"].column,
-  .ui.grid > [class*="ten wide large screen"].column,
-  .ui.column.grid > [class*="ten wide large screen"].column {
-    width: 62.5% !important;
-  }
-
-  .ui.grid > .row > [class*="eleven wide large screen"].column,
-  .ui.grid > .column.row > [class*="eleven wide large screen"].column,
-  .ui.grid > [class*="eleven wide large screen"].column,
-  .ui.column.grid > [class*="eleven wide large screen"].column {
-    width: 68.75% !important;
-  }
-
-  .ui.grid > .row > [class*="twelve wide large screen"].column,
-  .ui.grid > .column.row > [class*="twelve wide large screen"].column,
-  .ui.grid > [class*="twelve wide large screen"].column,
-  .ui.column.grid > [class*="twelve wide large screen"].column {
-    width: 75% !important;
-  }
-
-  .ui.grid > .row > [class*="thirteen wide large screen"].column,
-  .ui.grid > .column.row > [class*="thirteen wide large screen"].column,
-  .ui.grid > [class*="thirteen wide large screen"].column,
-  .ui.column.grid > [class*="thirteen wide large screen"].column {
-    width: 81.25% !important;
-  }
-
-  .ui.grid > .row > [class*="fourteen wide large screen"].column,
-  .ui.grid > .column.row > [class*="fourteen wide large screen"].column,
-  .ui.grid > [class*="fourteen wide large screen"].column,
-  .ui.column.grid > [class*="fourteen wide large screen"].column {
-    width: 87.5% !important;
-  }
-
-  .ui.grid > .row > [class*="fifteen wide large screen"].column,
-  .ui.grid > .column.row > [class*="fifteen wide large screen"].column,
-  .ui.grid > [class*="fifteen wide large screen"].column,
-  .ui.column.grid > [class*="fifteen wide large screen"].column {
-    width: 93.75% !important;
-  }
-
-  .ui.grid > .row > [class*="sixteen wide large screen"].column,
-  .ui.grid > .column.row > [class*="sixteen wide large screen"].column,
-  .ui.grid > [class*="sixteen wide large screen"].column,
-  .ui.column.grid > [class*="sixteen wide large screen"].column {
-    width: 100% !important;
-  }
-}
-
-/* Widescreen Sizing Combinations */
-
-@media only screen and (min-width: 1920px) {
-  .ui.grid > .row > [class*="one wide widescreen"].column,
-  .ui.grid > .column.row > [class*="one wide widescreen"].column,
-  .ui.grid > [class*="one wide widescreen"].column,
-  .ui.column.grid > [class*="one wide widescreen"].column {
-    width: 6.25% !important;
-  }
-
-  .ui.grid > .row > [class*="two wide widescreen"].column,
-  .ui.grid > .column.row > [class*="two wide widescreen"].column,
-  .ui.grid > [class*="two wide widescreen"].column,
-  .ui.column.grid > [class*="two wide widescreen"].column {
-    width: 12.5% !important;
-  }
-
-  .ui.grid > .row > [class*="three wide widescreen"].column,
-  .ui.grid > .column.row > [class*="three wide widescreen"].column,
-  .ui.grid > [class*="three wide widescreen"].column,
-  .ui.column.grid > [class*="three wide widescreen"].column {
-    width: 18.75% !important;
-  }
-
-  .ui.grid > .row > [class*="four wide widescreen"].column,
-  .ui.grid > .column.row > [class*="four wide widescreen"].column,
-  .ui.grid > [class*="four wide widescreen"].column,
-  .ui.column.grid > [class*="four wide widescreen"].column {
-    width: 25% !important;
-  }
-
-  .ui.grid > .row > [class*="five wide widescreen"].column,
-  .ui.grid > .column.row > [class*="five wide widescreen"].column,
-  .ui.grid > [class*="five wide widescreen"].column,
-  .ui.column.grid > [class*="five wide widescreen"].column {
-    width: 31.25% !important;
-  }
-
-  .ui.grid > .row > [class*="six wide widescreen"].column,
-  .ui.grid > .column.row > [class*="six wide widescreen"].column,
-  .ui.grid > [class*="six wide widescreen"].column,
-  .ui.column.grid > [class*="six wide widescreen"].column {
-    width: 37.5% !important;
-  }
-
-  .ui.grid > .row > [class*="seven wide widescreen"].column,
-  .ui.grid > .column.row > [class*="seven wide widescreen"].column,
-  .ui.grid > [class*="seven wide widescreen"].column,
-  .ui.column.grid > [class*="seven wide widescreen"].column {
-    width: 43.75% !important;
-  }
-
-  .ui.grid > .row > [class*="eight wide widescreen"].column,
-  .ui.grid > .column.row > [class*="eight wide widescreen"].column,
-  .ui.grid > [class*="eight wide widescreen"].column,
-  .ui.column.grid > [class*="eight wide widescreen"].column {
-    width: 50% !important;
-  }
-
-  .ui.grid > .row > [class*="nine wide widescreen"].column,
-  .ui.grid > .column.row > [class*="nine wide widescreen"].column,
-  .ui.grid > [class*="nine wide widescreen"].column,
-  .ui.column.grid > [class*="nine wide widescreen"].column {
-    width: 56.25% !important;
-  }
-
-  .ui.grid > .row > [class*="ten wide widescreen"].column,
-  .ui.grid > .column.row > [class*="ten wide widescreen"].column,
-  .ui.grid > [class*="ten wide widescreen"].column,
-  .ui.column.grid > [class*="ten wide widescreen"].column {
-    width: 62.5% !important;
-  }
-
-  .ui.grid > .row > [class*="eleven wide widescreen"].column,
-  .ui.grid > .column.row > [class*="eleven wide widescreen"].column,
-  .ui.grid > [class*="eleven wide widescreen"].column,
-  .ui.column.grid > [class*="eleven wide widescreen"].column {
-    width: 68.75% !important;
-  }
-
-  .ui.grid > .row > [class*="twelve wide widescreen"].column,
-  .ui.grid > .column.row > [class*="twelve wide widescreen"].column,
-  .ui.grid > [class*="twelve wide widescreen"].column,
-  .ui.column.grid > [class*="twelve wide widescreen"].column {
-    width: 75% !important;
-  }
-
-  .ui.grid > .row > [class*="thirteen wide widescreen"].column,
-  .ui.grid > .column.row > [class*="thirteen wide widescreen"].column,
-  .ui.grid > [class*="thirteen wide widescreen"].column,
-  .ui.column.grid > [class*="thirteen wide widescreen"].column {
-    width: 81.25% !important;
-  }
-
-  .ui.grid > .row > [class*="fourteen wide widescreen"].column,
-  .ui.grid > .column.row > [class*="fourteen wide widescreen"].column,
-  .ui.grid > [class*="fourteen wide widescreen"].column,
-  .ui.column.grid > [class*="fourteen wide widescreen"].column {
-    width: 87.5% !important;
-  }
-
-  .ui.grid > .row > [class*="fifteen wide widescreen"].column,
-  .ui.grid > .column.row > [class*="fifteen wide widescreen"].column,
-  .ui.grid > [class*="fifteen wide widescreen"].column,
-  .ui.column.grid > [class*="fifteen wide widescreen"].column {
-    width: 93.75% !important;
-  }
-
-  .ui.grid > .row > [class*="sixteen wide widescreen"].column,
-  .ui.grid > .column.row > [class*="sixteen wide widescreen"].column,
-  .ui.grid > [class*="sixteen wide widescreen"].column,
-  .ui.column.grid > [class*="sixteen wide widescreen"].column {
-    width: 100% !important;
-  }
-}
-
-/*----------------------
-          Centered
-  -----------------------*/
-
-.ui.centered.grid,
-.ui.centered.grid > .row,
-.ui.grid > .centered.row {
-  text-align: center;
-  justify-content: center;
-}
-
-.ui.centered.grid > .column:not(.aligned):not(.justified):not(.row),
-.ui.centered.grid > .row > .column:not(.aligned):not(.justified),
-.ui.grid .centered.row > .column:not(.aligned):not(.justified) {
-  text-align: left;
-}
-
-.ui.grid > .centered.column,
-.ui.grid > .row > .centered.column {
-  display: block;
-  margin-left: auto;
-  margin-right: auto;
-}
-
-/*----------------------
-          Relaxed
-  -----------------------*/
-
-.ui.relaxed.grid > .column:not(.row),
-.ui.relaxed.grid > .row > .column,
-.ui.grid > .relaxed.row > .column {
-  padding-left: 1.5rem;
-  padding-right: 1.5rem;
-}
-
-.ui[class*="very relaxed"].grid > .column:not(.row),
-.ui[class*="very relaxed"].grid > .row > .column,
-.ui.grid > [class*="very relaxed"].row > .column {
-  padding-left: 2.5rem;
-  padding-right: 2.5rem;
-}
-
-/* Coupling with UI Divider */
-
-.ui.relaxed.grid .row + .ui.divider,
-.ui.grid .relaxed.row + .ui.divider {
-  margin-left: 1.5rem;
-  margin-right: 1.5rem;
-}
-
-.ui[class*="very relaxed"].grid .row + .ui.divider,
-.ui.grid [class*="very relaxed"].row + .ui.divider {
-  margin-left: 2.5rem;
-  margin-right: 2.5rem;
-}
-
-/*----------------------
-          Padded
-  -----------------------*/
-
-.ui.padded.grid:not(.vertically):not(.horizontally) {
-  margin: 0 !important;
-}
-
-[class*="horizontally padded"].ui.grid {
-  margin-left: 0 !important;
-  margin-right: 0 !important;
-}
-
-[class*="vertically padded"].ui.grid {
-  margin-top: 0 !important;
-  margin-bottom: 0 !important;
-}
-
-/*----------------------
-         "Floated"
-  -----------------------*/
-
-.ui.grid [class*="left floated"].column {
-  margin-right: auto;
-}
-
-.ui.grid [class*="right floated"].column {
-  margin-left: auto;
-}
-
-/*----------------------
-          Divided
-  -----------------------*/
-
-.ui.divided.grid:not([class*="vertically divided"]) > .column:not(.row),
-.ui.divided.grid:not([class*="vertically divided"]) > .row > .column {
-  box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15);
-}
-
-/* Swap from padding to margin on columns to have dividers align */
-
-.ui[class*="vertically divided"].grid > .column:not(.row),
-.ui[class*="vertically divided"].grid > .row > .column {
-  margin-top: 1rem;
-  margin-bottom: 1rem;
-  padding-top: 0;
-  padding-bottom: 0;
-}
-
-.ui[class*="vertically divided"].grid > .row {
-  margin-top: 0;
-  margin-bottom: 0;
-}
-
-/* No divider on first column on row */
-
-.ui.divided.grid:not([class*="vertically divided"]) > .column:first-child,
-.ui.divided.grid:not([class*="vertically divided"]) > .row > .column:first-child {
-  box-shadow: none;
-}
-
-/* No space on top of first row */
-
-.ui[class*="vertically divided"].grid > .row:first-child > .column {
-  margin-top: 0;
-}
-
-/* Divided Row */
-
-.ui.grid > .divided.row > .column {
-  box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.grid > .divided.row > .column:first-child {
-  box-shadow: none;
-}
-
-/* Vertically Divided */
-
-.ui[class*="vertically divided"].grid > .row {
-  position: relative;
-}
-
-.ui[class*="vertically divided"].grid > .row:before {
-  position: absolute;
-  content: "";
-  top: 0;
-  left: 0;
-  width: calc(100% - 2rem);
-  height: 1px;
-  margin: 0 1rem;
-  box-shadow: 0 -1px 0 0 rgba(34, 36, 38, 0.15);
-}
-
-/* Padded Horizontally Divided */
-
-[class*="horizontally padded"].ui.divided.grid,
-.ui.padded.divided.grid:not(.vertically):not(.horizontally) {
-  width: 100%;
-}
-
-/* First Row Vertically Divided */
-
-.ui[class*="vertically divided"].grid > .row:first-child:before {
-  box-shadow: none;
-}
-
-/* Relaxed */
-
-.ui.relaxed[class*="vertically divided"].grid > .row:before {
-  margin-left: 1.5rem;
-  margin-right: 1.5rem;
-  width: calc(100% - 3rem);
-}
-
-.ui[class*="very relaxed"][class*="vertically divided"].grid > .row:before {
-  margin-left: 2.5rem;
-  margin-right: 2.5rem;
-  width: calc(100% - 5rem);
-}
-
-/*----------------------
-           Celled
-  -----------------------*/
-
-.ui.celled.grid {
-  width: 100%;
-  margin: 1em 0;
-  box-shadow: 0 0 0 1px #D4D4D5;
-}
-
-.ui.celled.grid > .row {
-  width: 100% !important;
-  margin: 0;
-  padding: 0;
-  box-shadow: 0 -1px 0 0 #D4D4D5;
-}
-
-.ui.celled.grid > .column:not(.row),
-.ui.celled.grid > .row > .column {
-  box-shadow: -1px 0 0 0 #D4D4D5;
-}
-
-.ui.celled.grid > .column:first-child,
-.ui.celled.grid > .row > .column:first-child {
-  box-shadow: none;
-}
-
-.ui.celled.grid > .column:not(.row),
-.ui.celled.grid > .row > .column {
-  padding: 1em;
-}
-
-.ui.relaxed.celled.grid > .column:not(.row),
-.ui.relaxed.celled.grid > .row > .column {
-  padding: 1.5em;
-}
-
-.ui[class*="very relaxed"].celled.grid > .column:not(.row),
-.ui[class*="very relaxed"].celled.grid > .row > .column {
-  padding: 2em;
-}
-
-/* Internally Celled */
-
-.ui[class*="internally celled"].grid {
-  box-shadow: none;
-  margin: 0;
-}
-
-.ui[class*="internally celled"].grid > .row:first-child {
-  box-shadow: none;
-}
-
-.ui[class*="internally celled"].grid > .row > .column:first-child {
-  box-shadow: none;
-}
-
-/*----------------------
-     Vertically Aligned
-  -----------------------*/
-
-/* Top Aligned */
-
-.ui[class*="top aligned"].grid > .column:not(.row),
-.ui[class*="top aligned"].grid > .row > .column,
-.ui.grid > [class*="top aligned"].row > .column,
-.ui.grid > [class*="top aligned"].column:not(.row),
-.ui.grid > .row > [class*="top aligned"].column {
-  flex-direction: column;
-  vertical-align: top;
-  align-self: flex-start !important;
-}
-
-/* Middle Aligned */
-
-.ui[class*="middle aligned"].grid > .column:not(.row),
-.ui[class*="middle aligned"].grid > .row > .column,
-.ui.grid > [class*="middle aligned"].row > .column,
-.ui.grid > [class*="middle aligned"].column:not(.row),
-.ui.grid > .row > [class*="middle aligned"].column {
-  flex-direction: column;
-  vertical-align: middle;
-  align-self: center !important;
-}
-
-/* Bottom Aligned */
-
-.ui[class*="bottom aligned"].grid > .column:not(.row),
-.ui[class*="bottom aligned"].grid > .row > .column,
-.ui.grid > [class*="bottom aligned"].row > .column,
-.ui.grid > [class*="bottom aligned"].column:not(.row),
-.ui.grid > .row > [class*="bottom aligned"].column {
-  flex-direction: column;
-  vertical-align: bottom;
-  align-self: flex-end !important;
-}
-
-/* Stretched */
-
-.ui.stretched.grid > .row > .column,
-.ui.stretched.grid > .column,
-.ui.grid > .stretched.row > .column,
-.ui.grid > .stretched.column:not(.row),
-.ui.grid > .row > .stretched.column {
-  display: inline-flex !important;
-  align-self: stretch;
-  flex-direction: column;
-}
-
-.ui.stretched.grid > .row > .column > *,
-.ui.stretched.grid > .column > *,
-.ui.grid > .stretched.row > .column > *,
-.ui.grid > .stretched.column:not(.row) > *,
-.ui.grid > .row > .stretched.column > * {
-  flex-grow: 1;
-}
-
-/*----------------------
-    Horizontally Centered
-  -----------------------*/
-
-/* Left Aligned */
-
-.ui[class*="left aligned"].grid > .column,
-.ui[class*="left aligned"].grid > .row > .column,
-.ui.grid > [class*="left aligned"].row > .column,
-.ui.grid > [class*="left aligned"].column.column,
-.ui.grid > .row > [class*="left aligned"].column.column {
-  text-align: left;
-  align-self: inherit;
-}
-
-/* Center Aligned */
-
-.ui[class*="center aligned"].grid > .column,
-.ui[class*="center aligned"].grid > .row > .column,
-.ui.grid > [class*="center aligned"].row > .column,
-.ui.grid > [class*="center aligned"].column.column,
-.ui.grid > .row > [class*="center aligned"].column.column {
-  text-align: center;
-  align-self: inherit;
-}
-
-.ui[class*="center aligned"].grid {
-  justify-content: center;
-}
-
-/* Right Aligned */
-
-.ui[class*="right aligned"].grid > .column,
-.ui[class*="right aligned"].grid > .row > .column,
-.ui.grid > [class*="right aligned"].row > .column,
-.ui.grid > [class*="right aligned"].column.column,
-.ui.grid > .row > [class*="right aligned"].column.column {
-  text-align: right;
-  align-self: inherit;
-}
-
-/* Justified */
-
-.ui.justified.grid > .column,
-.ui.justified.grid > .row > .column,
-.ui.grid > .justified.row > .column,
-.ui.grid > .justified.column.column,
-.ui.grid > .row > .justified.column.column {
-  text-align: justify;
-  -webkit-hyphens: auto;
-  hyphens: auto;
-}
-
-/*----------------------
-         Colored
------------------------*/
-
-.ui.grid > .primary.row,
-.ui.grid > .primary.column,
-.ui.grid > .row > .primary.column {
-  background-color: #2185D0;
-  color: #FFFFFF;
-}
-
-.ui.grid > .secondary.row,
-.ui.grid > .secondary.column,
-.ui.grid > .row > .secondary.column {
-  background-color: #1B1C1D;
-  color: #FFFFFF;
-}
-
-.ui.grid > .red.row,
-.ui.grid > .red.column,
-.ui.grid > .row > .red.column {
-  background-color: #DB2828;
-  color: #FFFFFF;
-}
-
-.ui.grid > .orange.row,
-.ui.grid > .orange.column,
-.ui.grid > .row > .orange.column {
-  background-color: #F2711C;
-  color: #FFFFFF;
-}
-
-.ui.grid > .yellow.row,
-.ui.grid > .yellow.column,
-.ui.grid > .row > .yellow.column {
-  background-color: #FBBD08;
-  color: #FFFFFF;
-}
-
-.ui.grid > .olive.row,
-.ui.grid > .olive.column,
-.ui.grid > .row > .olive.column {
-  background-color: #B5CC18;
-  color: #FFFFFF;
-}
-
-.ui.grid > .green.row,
-.ui.grid > .green.column,
-.ui.grid > .row > .green.column {
-  background-color: #21BA45;
-  color: #FFFFFF;
-}
-
-.ui.grid > .teal.row,
-.ui.grid > .teal.column,
-.ui.grid > .row > .teal.column {
-  background-color: #00B5AD;
-  color: #FFFFFF;
-}
-
-.ui.grid > .blue.row,
-.ui.grid > .blue.column,
-.ui.grid > .row > .blue.column {
-  background-color: #2185D0;
-  color: #FFFFFF;
-}
-
-.ui.grid > .violet.row,
-.ui.grid > .violet.column,
-.ui.grid > .row > .violet.column {
-  background-color: #6435C9;
-  color: #FFFFFF;
-}
-
-.ui.grid > .purple.row,
-.ui.grid > .purple.column,
-.ui.grid > .row > .purple.column {
-  background-color: #A333C8;
-  color: #FFFFFF;
-}
-
-.ui.grid > .pink.row,
-.ui.grid > .pink.column,
-.ui.grid > .row > .pink.column {
-  background-color: #E03997;
-  color: #FFFFFF;
-}
-
-.ui.grid > .brown.row,
-.ui.grid > .brown.column,
-.ui.grid > .row > .brown.column {
-  background-color: #A5673F;
-  color: #FFFFFF;
-}
-
-.ui.grid > .grey.row,
-.ui.grid > .grey.column,
-.ui.grid > .row > .grey.column {
-  background-color: #767676;
-  color: #FFFFFF;
-}
-
-.ui.grid > .black.row,
-.ui.grid > .black.column,
-.ui.grid > .row > .black.column {
-  background-color: #1B1C1D;
-  color: #FFFFFF;
-}
-
-/*----------------------
-      Equal Width
------------------------*/
-
-.ui[class*="equal width"].grid > .column:not(.row),
-.ui[class*="equal width"].grid > .row > .column,
-.ui.grid > [class*="equal width"].row > .column {
-  display: inline-block;
-  flex-grow: 1;
-}
-
-.ui[class*="equal width"].grid > .wide.column,
-.ui[class*="equal width"].grid > .row > .wide.column,
-.ui.grid > [class*="equal width"].row > .wide.column {
-  flex-grow: 0;
-}
-
-/*----------------------
-          Reverse
-  -----------------------*/
-
-/* Mobile */
-
-@media only screen and (max-width: 767.98px) {
-  .ui[class*="mobile reversed"].grid,
-  .ui[class*="mobile reversed"].grid > .row,
-  .ui.grid > [class*="mobile reversed"].row {
-    flex-direction: row-reverse;
-  }
-
-  .ui[class*="mobile vertically reversed"].grid,
-  .ui.stackable[class*="mobile reversed"] {
-    flex-direction: column-reverse;
-  }
-
-  /* Divided Reversed */
-
-  .ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"]) > .column:first-child,
-  .ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:first-child {
-    box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15);
-  }
-
-  .ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"]) > .column:last-child,
-  .ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:last-child {
-    box-shadow: none;
-  }
-
-  /* Vertically Divided Reversed */
-
-  .ui.grid[class*="vertically divided"][class*="mobile vertically reversed"] > .row:first-child:before {
-    box-shadow: 0 -1px 0 0 rgba(34, 36, 38, 0.15);
-  }
-
-  .ui.grid[class*="vertically divided"][class*="mobile vertically reversed"] > .row:last-child:before {
-    box-shadow: none;
-  }
-
-  /* Celled Reversed */
-
-  .ui[class*="mobile reversed"].celled.grid > .row > .column:first-child {
-    box-shadow: -1px 0 0 0 #D4D4D5;
-  }
-
-  .ui[class*="mobile reversed"].celled.grid > .row > .column:last-child {
-    box-shadow: none;
-  }
-}
-
-/* Tablet */
-
-@media only screen and (min-width: 768px) and (max-width: 991.98px) {
-  .ui[class*="tablet reversed"].grid,
-  .ui[class*="tablet reversed"].grid > .row,
-  .ui.grid > [class*="tablet reversed"].row {
-    flex-direction: row-reverse;
-  }
-
-  .ui[class*="tablet vertically reversed"].grid {
-    flex-direction: column-reverse;
-  }
-
-  /* Divided Reversed */
-
-  .ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"]) > .column:first-child,
-  .ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:first-child {
-    box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15);
-  }
-
-  .ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"]) > .column:last-child,
-  .ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:last-child {
-    box-shadow: none;
-  }
-
-  /* Vertically Divided Reversed */
-
-  .ui.grid[class*="vertically divided"][class*="tablet vertically reversed"] > .row:first-child:before {
-    box-shadow: 0 -1px 0 0 rgba(34, 36, 38, 0.15);
-  }
-
-  .ui.grid[class*="vertically divided"][class*="tablet vertically reversed"] > .row:last-child:before {
-    box-shadow: none;
-  }
-
-  /* Celled Reversed */
-
-  .ui[class*="tablet reversed"].celled.grid > .row > .column:first-child {
-    box-shadow: -1px 0 0 0 #D4D4D5;
-  }
-
-  .ui[class*="tablet reversed"].celled.grid > .row > .column:last-child {
-    box-shadow: none;
-  }
-}
-
-/* Computer */
-
-@media only screen and (min-width: 992px) {
-  .ui[class*="computer reversed"].grid,
-  .ui[class*="computer reversed"].grid > .row,
-  .ui.grid > [class*="computer reversed"].row {
-    flex-direction: row-reverse;
-  }
-
-  .ui[class*="computer vertically reversed"].grid {
-    flex-direction: column-reverse;
-  }
-
-  /* Divided Reversed */
-
-  .ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"]) > .column:first-child,
-  .ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:first-child {
-    box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15);
-  }
-
-  .ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"]) > .column:last-child,
-  .ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:last-child {
-    box-shadow: none;
-  }
-
-  /* Vertically Divided Reversed */
-
-  .ui.grid[class*="vertically divided"][class*="computer vertically reversed"] > .row:first-child:before {
-    box-shadow: 0 -1px 0 0 rgba(34, 36, 38, 0.15);
-  }
-
-  .ui.grid[class*="vertically divided"][class*="computer vertically reversed"] > .row:last-child:before {
-    box-shadow: none;
-  }
-
-  /* Celled Reversed */
-
-  .ui[class*="computer reversed"].celled.grid > .row > .column:first-child {
-    box-shadow: -1px 0 0 0 #D4D4D5;
-  }
-
-  .ui[class*="computer reversed"].celled.grid > .row > .column:last-child {
-    box-shadow: none;
-  }
-}
-
-/*-------------------
-        Stackable
-  --------------------*/
-
-@media only screen and (max-width: 767.98px) {
-  .ui.stackable.grid {
-    width: auto;
-    margin-left: 0 !important;
-    margin-right: 0 !important;
-  }
-
-  .ui.stackable.grid > .row > .wide.column,
-  .ui.stackable.grid > .wide.column,
-  .ui.stackable.grid > .column.grid > .column,
-  .ui.stackable.grid > .column.row > .column,
-  .ui.stackable.grid > .row > .column,
-  .ui.stackable.grid > .column:not(.row),
-  .ui.grid > .stackable.stackable.stackable.row > .column {
-    width: 100% !important;
-    margin: 0 0 !important;
-    box-shadow: none !important;
-    padding: 1rem 1rem;
-  }
-
-  .ui.stackable.grid:not(.vertically) > .row {
-    margin: 0;
-    padding: 0;
-  }
-
-  /* Coupling */
-
-  .ui.container > .ui.stackable.grid > .column,
-  .ui.container > .ui.stackable.grid > .row > .column {
-    padding-left: 0 !important;
-    padding-right: 0 !important;
-  }
-
-  /* Don't pad inside segment or nested grid */
-
-  .ui.grid .ui.stackable.grid,
-  .ui.segment:not(.vertical) .ui.stackable.page.grid {
-    margin-left: -1rem !important;
-    margin-right: -1rem !important;
-  }
-
-  /* Divided Stackable */
-
-  .ui.stackable.divided.grid > .row:first-child > .column:first-child,
-  .ui.stackable.celled.grid > .row:first-child > .column:first-child,
-  .ui.stackable.divided.grid > .column:not(.row):first-child,
-  .ui.stackable.celled.grid > .column:not(.row):first-child {
-    border-top: none !important;
-  }
-
-  .ui.stackable.celled.grid > .column:not(.row),
-  .ui.stackable.divided:not(.vertically).grid > .column:not(.row),
-  .ui.stackable.celled.grid > .row > .column,
-  .ui.stackable.divided:not(.vertically).grid > .row > .column {
-    border-top: 1px solid rgba(34, 36, 38, 0.15);
-    box-shadow: none !important;
-    padding-top: 2rem !important;
-    padding-bottom: 2rem !important;
-  }
-
-  .ui.stackable.celled.grid > .row {
-    box-shadow: none !important;
-  }
-
-  .ui.stackable.divided:not(.vertically).grid > .column:not(.row),
-  .ui.stackable.divided:not(.vertically).grid > .row > .column {
-    padding-left: 0 !important;
-    padding-right: 0 !important;
-  }
-}
-
-/*----------------------
-     Only (Device)
------------------------*/
-
-/* These include arbitrary class repetitions for forced specificity */
-
-/* Mobile Only Hide */
-
-@media only screen and (max-width: 767.98px) {
-  .ui[class*="tablet only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="tablet only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="tablet only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="tablet only"].column:not(.mobile) {
-    display: none !important;
-  }
-
-  .ui[class*="computer only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="computer only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="computer only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="computer only"].column:not(.mobile) {
-    display: none !important;
-  }
-
-  .ui[class*="large screen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="large screen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="large screen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="large screen only"].column:not(.mobile) {
-    display: none !important;
-  }
-
-  .ui[class*="widescreen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="widescreen only"].column:not(.mobile) {
-    display: none !important;
-  }
-}
-
-/* Tablet Only Hide */
-
-@media only screen and (min-width: 768px) and (max-width: 991.98px) {
-  .ui[class*="mobile only"].grid.grid.grid:not(.tablet),
-  .ui.grid.grid.grid > [class*="mobile only"].row:not(.tablet),
-  .ui.grid.grid.grid > [class*="mobile only"].column:not(.tablet),
-  .ui.grid.grid.grid > .row > [class*="mobile only"].column:not(.tablet) {
-    display: none !important;
-  }
-
-  .ui[class*="computer only"].grid.grid.grid:not(.tablet),
-  .ui.grid.grid.grid > [class*="computer only"].row:not(.tablet),
-  .ui.grid.grid.grid > [class*="computer only"].column:not(.tablet),
-  .ui.grid.grid.grid > .row > [class*="computer only"].column:not(.tablet) {
-    display: none !important;
-  }
-
-  .ui[class*="large screen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="large screen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="large screen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="large screen only"].column:not(.mobile) {
-    display: none !important;
-  }
-
-  .ui[class*="widescreen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="widescreen only"].column:not(.mobile) {
-    display: none !important;
-  }
-}
-
-/* Computer Only Hide */
-
-@media only screen and (min-width: 992px) and (max-width: 1199.98px) {
-  .ui[class*="mobile only"].grid.grid.grid:not(.computer),
-  .ui.grid.grid.grid > [class*="mobile only"].row:not(.computer),
-  .ui.grid.grid.grid > [class*="mobile only"].column:not(.computer),
-  .ui.grid.grid.grid > .row > [class*="mobile only"].column:not(.computer) {
-    display: none !important;
-  }
-
-  .ui[class*="tablet only"].grid.grid.grid:not(.computer),
-  .ui.grid.grid.grid > [class*="tablet only"].row:not(.computer),
-  .ui.grid.grid.grid > [class*="tablet only"].column:not(.computer),
-  .ui.grid.grid.grid > .row > [class*="tablet only"].column:not(.computer) {
-    display: none !important;
-  }
-
-  .ui[class*="large screen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="large screen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="large screen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="large screen only"].column:not(.mobile) {
-    display: none !important;
-  }
-
-  .ui[class*="widescreen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="widescreen only"].column:not(.mobile) {
-    display: none !important;
-  }
-}
-
-/* Large Screen Only Hide */
-
-@media only screen and (min-width: 1200px) and (max-width: 1919.98px) {
-  .ui[class*="mobile only"].grid.grid.grid:not(.computer),
-  .ui.grid.grid.grid > [class*="mobile only"].row:not(.computer),
-  .ui.grid.grid.grid > [class*="mobile only"].column:not(.computer),
-  .ui.grid.grid.grid > .row > [class*="mobile only"].column:not(.computer) {
-    display: none !important;
-  }
-
-  .ui[class*="tablet only"].grid.grid.grid:not(.computer),
-  .ui.grid.grid.grid > [class*="tablet only"].row:not(.computer),
-  .ui.grid.grid.grid > [class*="tablet only"].column:not(.computer),
-  .ui.grid.grid.grid > .row > [class*="tablet only"].column:not(.computer) {
-    display: none !important;
-  }
-
-  .ui[class*="widescreen only"].grid.grid.grid:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].row:not(.mobile),
-  .ui.grid.grid.grid > [class*="widescreen only"].column:not(.mobile),
-  .ui.grid.grid.grid > .row > [class*="widescreen only"].column:not(.mobile) {
-    display: none !important;
-  }
-}
-
-/* Widescreen Only Hide */
-
-@media only screen and (min-width: 1920px) {
-  .ui[class*="mobile only"].grid.grid.grid:not(.computer),
-  .ui.grid.grid.grid > [class*="mobile only"].row:not(.computer),
-  .ui.grid.grid.grid > [class*="mobile only"].column:not(.computer),
-  .ui.grid.grid.grid > .row > [class*="mobile only"].column:not(.computer) {
-    display: none !important;
-  }
-
-  .ui[class*="tablet only"].grid.grid.grid:not(.computer),
-  .ui.grid.grid.grid > [class*="tablet only"].row:not(.computer),
-  .ui.grid.grid.grid > [class*="tablet only"].column:not(.computer),
-  .ui.grid.grid.grid > .row > [class*="tablet only"].column:not(.computer) {
-    display: none !important;
-  }
-}
-
-/*-----------------
-        Compact
-  -----------------*/
-
-.ui.ui.ui.compact.grid > .column:not(.row),
-.ui.ui.ui.compact.grid > .row > .column {
-  padding-left: 0.5rem;
-  padding-right: 0.5rem;
-}
-
-.ui.ui.ui.compact.grid > * {
-  padding-left: 0.5rem;
-  padding-right: 0.5rem;
-}
-
-/* Row */
-
-.ui.ui.ui.compact.grid > .row {
-  padding-top: 0.5rem;
-  padding-bottom: 0.5rem;
-}
-
-/* Columns */
-
-.ui.ui.ui.compact.grid > .column:not(.row) {
-  padding-top: 0.5rem;
-  padding-bottom: 0.5rem;
-}
-
-/* Relaxed + Celled */
-
-.ui.compact.relaxed.celled.grid > .column:not(.row),
-.ui.compact.relaxed.celled.grid > .row > .column {
-  padding: 0.75em;
-}
-
-.ui.compact[class*="very relaxed"].celled.grid > .column:not(.row),
-.ui.compact[class*="very relaxed"].celled.grid > .row > .column {
-  padding: 1em;
-}
-
-/*-----------------
-      Very compact
-  -----------------*/
-
-.ui.ui.ui[class*="very compact"].grid > .column:not(.row),
-.ui.ui.ui[class*="very compact"].grid > .row > .column {
-  padding-left: 0.25rem;
-  padding-right: 0.25rem;
-}
-
-.ui.ui.ui[class*="very compact"].grid > * {
-  padding-left: 0.25rem;
-  padding-right: 0.25rem;
-}
-
-/* Row */
-
-.ui.ui.ui[class*="very compact"].grid > .row {
-  padding-top: 0.25rem;
-  padding-bottom: 0.25rem;
-  padding-left: 0.75rem;
-  padding-right: 0.75rem;
-}
-
-/* Columns */
-
-.ui.ui.ui[class*="very compact"].grid > .column:not(.row) {
-  padding-top: 0.25rem;
-  padding-bottom: 0.25rem;
-}
-
-/* Relaxed + Celled */
-
-.ui[class*="very compact"].relaxed.celled.grid > .column:not(.row),
-.ui[class*="very compact"].relaxed.celled.grid > .row > .column {
-  padding: 0.375em;
-}
-
-.ui[class*="very compact"][class*="very relaxed"].celled.grid > .column:not(.row),
-.ui[class*="very compact"][class*="very relaxed"].celled.grid > .row > .column {
-  padding: 0.5em;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Header
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            Header
-*******************************/
-
-/* Standard */
-
-.ui.header {
-  border: none;
-  margin: calc(2rem - 0.1428571428571429em) 0 1rem;
-  padding: 0 0;
-  font-family: var(--fonts-regular);
-  font-weight: 500;
-  line-height: 1.28571429em;
-  text-transform: none;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-.ui.header:first-child {
-  margin-top: -0.14285714em;
-}
-
-.ui.header:last-child {
-  margin-bottom: 0;
-}
-
-/*--------------
-     Sub Header
-  ---------------*/
-
-.ui.header .sub.header {
-  display: block;
-  font-weight: normal;
-  padding: 0;
-  margin: 0;
-  font-size: 1rem;
-  line-height: 1.2em;
-  color: rgba(0, 0, 0, 0.6);
-}
-
-/*--------------
-      Icon
----------------*/
-
-.ui.header > i.icon {
-  display: table-cell;
-  opacity: 1;
-  font-size: 1.5em;
-  padding-top: 0;
-  vertical-align: middle;
-}
-
-/* With Text Node */
-
-.ui.header > i.icon:only-child {
-  display: inline-block;
-  padding: 0;
-  margin-right: 0.75rem;
-}
-
-/*-------------------
-        Image
---------------------*/
-
-.ui.header > .image:not(.icon),
-.ui.header > img {
-  display: inline-block;
-  margin-top: 0.14285714em;
-  width: 2.5em;
-  height: auto;
-  vertical-align: middle;
-}
-
-.ui.header > .image:not(.icon):only-child,
-.ui.header > img:only-child {
-  margin-right: 0.75rem;
-}
-
-/*--------------
-     Content
----------------*/
-
-.ui.header .content {
-  display: inline-block;
-  vertical-align: top;
-}
-
-/* After Image */
-
-.ui.header > img + .content,
-.ui.header > .image + .content {
-  padding-left: 0.75rem;
-  vertical-align: middle;
-}
-
-/* After Icon */
-
-.ui.header > i.icon + .content {
-  padding-left: 0.75rem;
-  display: table-cell;
-  vertical-align: middle;
-}
-
-/*--------------
- Loose Coupling
----------------*/
-
-.ui.header .ui.label {
-  font-size: '';
-  margin-left: 0.5rem;
-  vertical-align: middle;
-}
-
-/* Positioning */
-
-.ui.header + p {
-  margin-top: 0;
-}
-
-/*******************************
-            Types
-*******************************/
-
-/*--------------
-     Page
----------------*/
-
-h1.ui.header {
-  font-size: 2rem;
-}
-
-h1.ui.header .sub.header {
-  font-size: 1.14285714rem;
-}
-
-h2.ui.header {
-  font-size: 1.71428571rem;
-}
-
-h2.ui.header .sub.header {
-  font-size: 1.14285714rem;
-}
-
-h3.ui.header {
-  font-size: 1.28571429rem;
-}
-
-h3.ui.header .sub.header {
-  font-size: 1rem;
-}
-
-h4.ui.header {
-  font-size: 1.07142857rem;
-}
-
-h4.ui.header .sub.header {
-  font-size: 1rem;
-}
-
-h5.ui.header {
-  font-size: 1rem;
-}
-
-h5.ui.header .sub.header {
-  font-size: 0.92857143rem;
-}
-
-h6.ui.header {
-  font-size: 0.85714286rem;
-}
-
-h6.ui.header .sub.header {
-  font-size: 0.92857143rem;
-}
-
-/*--------------
- Content Heading
----------------*/
-
-.ui.mini.header {
-  font-size: 0.85714286em;
-}
-
-.ui.mini.header .sub.header {
-  font-size: 0.92857143rem;
-}
-
-.ui.mini.sub.header {
-  font-size: 0.78571429em;
-}
-
-.ui.tiny.header {
-  font-size: 1em;
-}
-
-.ui.tiny.header .sub.header {
-  font-size: 0.92857143rem;
-}
-
-.ui.tiny.sub.header {
-  font-size: 0.78571429em;
-}
-
-.ui.small.header {
-  font-size: 1.07142857em;
-}
-
-.ui.small.header .sub.header {
-  font-size: 1rem;
-}
-
-.ui.small.sub.header {
-  font-size: 0.78571429em;
-}
-
-.ui.large.header {
-  font-size: 1.71428571em;
-}
-
-.ui.large.header .sub.header {
-  font-size: 1.14285714rem;
-}
-
-.ui.large.sub.header {
-  font-size: 0.92857143em;
-}
-
-.ui.big.header {
-  font-size: 1.85714286em;
-}
-
-.ui.big.header .sub.header {
-  font-size: 1.14285714rem;
-}
-
-.ui.big.sub.header {
-  font-size: 1em;
-}
-
-.ui.huge.header {
-  font-size: 2em;
-  min-height: 1em;
-}
-
-.ui.huge.header .sub.header {
-  font-size: 1.14285714rem;
-}
-
-.ui.huge.sub.header {
-  font-size: 1em;
-}
-
-.ui.massive.header {
-  font-size: 2.28571429em;
-  min-height: 1em;
-}
-
-.ui.massive.header .sub.header {
-  font-size: 1.42857143rem;
-}
-
-.ui.massive.sub.header {
-  font-size: 1.14285714em;
-}
-
-/*--------------
-     Sub Heading
-  ---------------*/
-
-.ui.sub.header {
-  padding: 0;
-  margin-bottom: 0.14285714rem;
-  font-weight: 500;
-  font-size: 0.85714286em;
-  text-transform: uppercase;
-  color: '';
-}
-
-/*-------------------
-          Icon
-  --------------------*/
-
-.ui.icon.header {
-  display: inline-block;
-  text-align: center;
-  margin: 2rem 0 1rem;
-}
-
-.ui.icon.header:after {
-  content: '';
-  display: block;
-  height: 0;
-  clear: both;
-  visibility: hidden;
-}
-
-.ui.icon.header:first-child {
-  margin-top: 0;
-}
-
-.ui.icon.header > i.icon {
-  float: none;
-  display: block;
-  width: auto;
-  height: auto;
-  line-height: 1;
-  padding: 0;
-  font-size: 3em;
-  margin: 0 auto 0.5rem;
-  opacity: 1;
-}
-
-.ui.icon.header .corner.icon {
-  font-size: calc(3em * 0.45);
-}
-
-.ui.icon.header .content {
-  display: block;
-  padding: 0;
-}
-
-.ui.icon.header > i.circular.icon {
-  font-size: 2em;
-}
-
-.ui.icon.header > i.square.icon {
-  font-size: 2em;
-}
-
-.ui.block.icon.header > i.icon {
-  margin-bottom: 0;
-}
-
-.ui.icon.header.aligned {
-  margin-left: auto;
-  margin-right: auto;
-  display: block;
-}
-
-/*******************************
-            States
-*******************************/
-
-.ui.disabled.header {
-  opacity: var(--opacity-disabled);
-}
-
-/*******************************
-           Variations
-*******************************/
-
-/*-------------------
-       Colors
---------------------*/
-
-.ui.primary.header {
-  color: #2185D0;
-}
-
-a.ui.primary.header:hover {
-  color: #1678c2;
-}
-
-.ui.primary.dividing.header {
-  border-bottom: 2px solid #2185D0;
-}
-
-.ui.secondary.header {
-  color: #1B1C1D;
-}
-
-a.ui.secondary.header:hover {
-  color: #27292a;
-}
-
-.ui.secondary.dividing.header {
-  border-bottom: 2px solid #1B1C1D;
-}
-
-.ui.red.header {
-  color: #DB2828;
-}
-
-a.ui.red.header:hover {
-  color: #d01919;
-}
-
-.ui.red.dividing.header {
-  border-bottom: 2px solid #DB2828;
-}
-
-.ui.orange.header {
-  color: #F2711C;
-}
-
-a.ui.orange.header:hover {
-  color: #f26202;
-}
-
-.ui.orange.dividing.header {
-  border-bottom: 2px solid #F2711C;
-}
-
-.ui.yellow.header {
-  color: #FBBD08;
-}
-
-a.ui.yellow.header:hover {
-  color: #eaae00;
-}
-
-.ui.yellow.dividing.header {
-  border-bottom: 2px solid #FBBD08;
-}
-
-.ui.olive.header {
-  color: #B5CC18;
-}
-
-a.ui.olive.header:hover {
-  color: #a7bd0d;
-}
-
-.ui.olive.dividing.header {
-  border-bottom: 2px solid #B5CC18;
-}
-
-.ui.green.header {
-  color: #21BA45;
-}
-
-a.ui.green.header:hover {
-  color: #16ab39;
-}
-
-.ui.green.dividing.header {
-  border-bottom: 2px solid #21BA45;
-}
-
-.ui.teal.header {
-  color: #00B5AD;
-}
-
-a.ui.teal.header:hover {
-  color: #009c95;
-}
-
-.ui.teal.dividing.header {
-  border-bottom: 2px solid #00B5AD;
-}
-
-.ui.blue.header {
-  color: #2185D0;
-}
-
-a.ui.blue.header:hover {
-  color: #1678c2;
-}
-
-.ui.blue.dividing.header {
-  border-bottom: 2px solid #2185D0;
-}
-
-.ui.violet.header {
-  color: #6435C9;
-}
-
-a.ui.violet.header:hover {
-  color: #5829bb;
-}
-
-.ui.violet.dividing.header {
-  border-bottom: 2px solid #6435C9;
-}
-
-.ui.purple.header {
-  color: #A333C8;
-}
-
-a.ui.purple.header:hover {
-  color: #9627ba;
-}
-
-.ui.purple.dividing.header {
-  border-bottom: 2px solid #A333C8;
-}
-
-.ui.pink.header {
-  color: #E03997;
-}
-
-a.ui.pink.header:hover {
-  color: #e61a8d;
-}
-
-.ui.pink.dividing.header {
-  border-bottom: 2px solid #E03997;
-}
-
-.ui.brown.header {
-  color: #A5673F;
-}
-
-a.ui.brown.header:hover {
-  color: #975b33;
-}
-
-.ui.brown.dividing.header {
-  border-bottom: 2px solid #A5673F;
-}
-
-.ui.grey.header {
-  color: #767676;
-}
-
-a.ui.grey.header:hover {
-  color: #838383;
-}
-
-.ui.grey.dividing.header {
-  border-bottom: 2px solid #767676;
-}
-
-.ui.black.header {
-  color: #1B1C1D;
-}
-
-a.ui.black.header:hover {
-  color: #27292a;
-}
-
-.ui.black.dividing.header {
-  border-bottom: 2px solid #1B1C1D;
-}
-
-/*-------------------
-         Aligned
-  --------------------*/
-
-.ui.left.aligned.header {
-  text-align: left;
-}
-
-.ui.right.aligned.header {
-  text-align: right;
-}
-
-.ui.centered.header,
-.ui.center.aligned.header {
-  text-align: center;
-}
-
-.ui.justified.header {
-  text-align: justify;
-}
-
-.ui.justified.header:after {
-  display: inline-block;
-  content: '';
-  width: 100%;
-}
-
-/*-------------------
-         Floated
-  --------------------*/
-
-.ui.floated.header,
-.ui[class*="left floated"].header {
-  float: left;
-  margin-top: 0;
-  margin-right: 0.5em;
-}
-
-.ui[class*="right floated"].header {
-  float: right;
-  margin-top: 0;
-  margin-left: 0.5em;
-}
-
-/*-------------------
-         Fitted
-  --------------------*/
-
-.ui.fitted.header {
-  padding: 0;
-}
-
-/*-------------------
-        Dividing
-  --------------------*/
-
-.ui.dividing.header {
-  padding-bottom: 0.21428571rem;
-  border-bottom: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.dividing.header .sub.header {
-  padding-bottom: 0.21428571rem;
-}
-
-.ui.dividing.header i.icon {
-  margin-bottom: 0;
-}
-
-/*-------------------
-          Block
-  --------------------*/
-
-.ui.block.header {
-  background: #F3F4F5;
-  padding: 0.78571429rem 1rem;
-  box-shadow: none;
-  border: 1px solid #D4D4D5;
-  border-radius: 0.28571429rem;
-}
-
-.ui.block.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) {
-  font-size: 1rem;
-}
-
-.ui.mini.block.header {
-  font-size: 0.78571429rem;
-}
-
-.ui.tiny.block.header {
-  font-size: 0.85714286rem;
-}
-
-.ui.small.block.header {
-  font-size: 0.92857143rem;
-}
-
-.ui.large.block.header {
-  font-size: 1.14285714rem;
-}
-
-.ui.big.block.header {
-  font-size: 1.28571429rem;
-}
-
-.ui.huge.block.header {
-  font-size: 1.42857143rem;
-}
-
-.ui.massive.block.header {
-  font-size: 1.71428571rem;
-}
-
-/*-------------------
-         Attached
-  --------------------*/
-
-.ui.attached.header {
-  background: #FFFFFF;
-  padding: 0.78571429rem 1rem;
-  margin: 0 -1px 0 -1px;
-  box-shadow: none;
-  border: 1px solid #D4D4D5;
-  border-radius: 0;
-}
-
-.ui.attached.block.header {
-  background: #F3F4F5;
-}
-
-.ui.attached:not(.top).header {
-  border-top: none;
-}
-
-.ui.top.attached.header {
-  border-radius: 0.28571429rem 0.28571429rem 0 0;
-}
-
-.ui.bottom.attached.header {
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
-}
-
-/* Attached Sizes */
-
-.ui.attached.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) {
-  font-size: 1em;
-}
-
-.ui.mini.attached.header {
-  font-size: 0.78571429em;
-}
-
-.ui.tiny.attached.header {
-  font-size: 0.85714286em;
-}
-
-.ui.small.attached.header {
-  font-size: 0.92857143em;
-}
-
-.ui.large.attached.header {
-  font-size: 1.14285714em;
-}
-
-.ui.big.attached.header {
-  font-size: 1.28571429em;
-}
-
-.ui.huge.attached.header {
-  font-size: 1.42857143em;
-}
-
-.ui.massive.attached.header {
-  font-size: 1.71428571em;
-}
-
-/*-------------------
-        Sizing
---------------------*/
-
-.ui.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) {
-  font-size: 1.28571429em;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Input
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-           Standard
-*******************************/
-
-/*--------------------
-        Inputs
----------------------*/
-
-.ui.input {
-  position: relative;
-  font-weight: normal;
-  font-style: normal;
-  display: inline-flex;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-.ui.input > input {
-  margin: 0;
-  max-width: 100%;
-  flex: 1 0 auto;
-  outline: none;
-  -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
-  text-align: left;
-  line-height: 1.21428571em;
-  font-family: var(--fonts-regular);
-  padding: 0.67857143em 1em;
-  background: #FFFFFF;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-  color: rgba(0, 0, 0, 0.87);
-  border-radius: 0.28571429rem;
-  transition: box-shadow 0.1s ease, border-color 0.1s ease;
-  box-shadow: none;
-}
-
-/*--------------------
-      Placeholder
----------------------*/
-
-/* browsers require these rules separate */
-
-.ui.input > input::-webkit-input-placeholder {
-  color: rgba(191, 191, 191, 0.87);
-}
-
-.ui.input > input::-moz-placeholder {
-  color: rgba(191, 191, 191, 0.87);
-}
-
-.ui.input > input:-ms-input-placeholder {
-  color: rgba(191, 191, 191, 0.87);
-}
-
-/*******************************
-            States
-*******************************/
-
-/*--------------------
-          Disabled
-  ---------------------*/
-
-.ui.disabled.input,
-.ui.input:not(.disabled) input[disabled] {
-  opacity: var(--opacity-disabled);
-}
-
-.ui.disabled.input > input,
-.ui.input:not(.disabled) input[disabled] {
-  pointer-events: none;
-}
-
-/*--------------------
-        Active
----------------------*/
-
-.ui.input > input:active,
-.ui.input.down input {
-  border-color: rgba(0, 0, 0, 0.3);
-  background: #FAFAFA;
-  color: rgba(0, 0, 0, 0.87);
-  box-shadow: none;
-}
-
-/*--------------------
-         Loading
-  ---------------------*/
-
-.ui.loading.loading.input > i.icon:before {
-  position: absolute;
-  content: '';
-  top: 50%;
-  left: 50%;
-  margin: -0.64285714em 0 0 -0.64285714em;
-  width: 1.28571429em;
-  height: 1.28571429em;
-  border-radius: 500rem;
-  border: 0.2em solid rgba(0, 0, 0, 0.1);
-}
-
-.ui.loading.loading.input > i.icon:after {
-  position: absolute;
-  content: '';
-  top: 50%;
-  left: 50%;
-  margin: -0.64285714em 0 0 -0.64285714em;
-  width: 1.28571429em;
-  height: 1.28571429em;
-  -webkit-animation: loader 0.6s infinite linear;
-  animation: loader 0.6s infinite linear;
-  border: 0.2em solid #767676;
-  border-radius: 500rem;
-  box-shadow: 0 0 0 1px transparent;
-}
-
-/*--------------------
-        Focus
----------------------*/
-
-.ui.input.focus > input,
-.ui.input > input:focus {
-  border-color: #85B7D9;
-  background: #FFFFFF;
-  color: rgba(0, 0, 0, 0.8);
-  box-shadow: none;
-}
-
-.ui.input.focus > input::-webkit-input-placeholder,
-.ui.input > input:focus::-webkit-input-placeholder {
-  color: rgba(115, 115, 115, 0.87);
-}
-
-.ui.input.focus > input::-moz-placeholder,
-.ui.input > input:focus::-moz-placeholder {
-  color: rgba(115, 115, 115, 0.87);
-}
-
-.ui.input.focus > input:-ms-input-placeholder,
-.ui.input > input:focus:-ms-input-placeholder {
-  color: rgba(115, 115, 115, 0.87);
-}
-
-/*--------------------
-          States
-  ---------------------*/
-
-.ui.input.error > input {
-  background-color: #FFF6F6;
-  border-color: #E0B4B4;
-  color: #9F3A38;
-  box-shadow: none;
-}
-
-/* Placeholder */
-
-.ui.input.error > input::-webkit-input-placeholder {
-  color: #e7bdbc;
-}
-
-.ui.input.error > input::-moz-placeholder {
-  color: #e7bdbc;
-}
-
-.ui.input.error > input:-ms-input-placeholder {
-  color: #e7bdbc !important;
-}
-
-/* Focused Placeholder */
-
-.ui.input.error > input:focus::-webkit-input-placeholder {
-  color: #da9796;
-}
-
-.ui.input.error > input:focus::-moz-placeholder {
-  color: #da9796;
-}
-
-.ui.input.error > input:focus:-ms-input-placeholder {
-  color: #da9796 !important;
-}
-
-.ui.input.info > input {
-  background-color: #F8FFFF;
-  border-color: #A9D5DE;
-  color: #276F86;
-  box-shadow: none;
-}
-
-/* Placeholder */
-
-.ui.input.info > input::-webkit-input-placeholder {
-  color: #98cfe1;
-}
-
-.ui.input.info > input::-moz-placeholder {
-  color: #98cfe1;
-}
-
-.ui.input.info > input:-ms-input-placeholder {
-  color: #98cfe1 !important;
-}
-
-/* Focused Placeholder */
-
-.ui.input.info > input:focus::-webkit-input-placeholder {
-  color: #70bdd6;
-}
-
-.ui.input.info > input:focus::-moz-placeholder {
-  color: #70bdd6;
-}
-
-.ui.input.info > input:focus:-ms-input-placeholder {
-  color: #70bdd6 !important;
-}
-
-.ui.input.success > input {
-  background-color: #FCFFF5;
-  border-color: #A3C293;
-  color: #2C662D;
-  box-shadow: none;
-}
-
-/* Placeholder */
-
-.ui.input.success > input::-webkit-input-placeholder {
-  color: #8fcf90;
-}
-
-.ui.input.success > input::-moz-placeholder {
-  color: #8fcf90;
-}
-
-.ui.input.success > input:-ms-input-placeholder {
-  color: #8fcf90 !important;
-}
-
-/* Focused Placeholder */
-
-.ui.input.success > input:focus::-webkit-input-placeholder {
-  color: #6cbf6d;
-}
-
-.ui.input.success > input:focus::-moz-placeholder {
-  color: #6cbf6d;
-}
-
-.ui.input.success > input:focus:-ms-input-placeholder {
-  color: #6cbf6d !important;
-}
-
-.ui.input.warning > input {
-  background-color: #FFFAF3;
-  border-color: #C9BA9B;
-  color: #573A08;
-  box-shadow: none;
-}
-
-/* Placeholder */
-
-.ui.input.warning > input::-webkit-input-placeholder {
-  color: #edad3e;
-}
-
-.ui.input.warning > input::-moz-placeholder {
-  color: #edad3e;
-}
-
-.ui.input.warning > input:-ms-input-placeholder {
-  color: #edad3e !important;
-}
-
-/* Focused Placeholder */
-
-.ui.input.warning > input:focus::-webkit-input-placeholder {
-  color: #e39715;
-}
-
-.ui.input.warning > input:focus::-moz-placeholder {
-  color: #e39715;
-}
-
-.ui.input.warning > input:focus:-ms-input-placeholder {
-  color: #e39715 !important;
-}
-
-/*******************************
-           Variations
-*******************************/
-
-/*--------------------
-        Transparent
-  ---------------------*/
-
-.ui.transparent.input > textarea,
-.ui.transparent.input > input {
-  border-color: transparent !important;
-  background-color: transparent !important;
-  padding: 0;
-  box-shadow: none !important;
-  border-radius: 0 !important;
-}
-
-.field .ui.transparent.input > textarea {
-  padding: 0.67857143em 1em;
-}
-
-/* Transparent Icon */
-
-:not(.field) > .ui.transparent.icon.input > i.icon {
-  width: 1.1em;
-}
-
-:not(.field) > .ui.ui.ui.transparent.icon.input > input {
-  padding-left: 0;
-  padding-right: 2em;
-}
-
-:not(.field) > .ui.ui.ui.transparent[class*="left icon"].input > input {
-  padding-left: 2em;
-  padding-right: 0;
-}
-
-/*--------------------
-           Icon
-  ---------------------*/
-
-.ui.icon.input > i.icon {
-  cursor: default;
-  position: absolute;
-  line-height: 1;
-  text-align: center;
-  top: 0;
-  right: 0;
-  margin: 0;
-  height: 100%;
-  width: 2.67142857em;
-  opacity: 0.5;
-  border-radius: 0 0.28571429rem 0.28571429rem 0;
-  transition: opacity 0.3s ease;
-}
-
-.ui.icon.input > i.icon:not(.link) {
-  pointer-events: none;
-}
-
-.ui.ui.ui.ui.icon.input > textarea,
-.ui.ui.ui.ui.icon.input > input {
-  padding-right: 2.67142857em;
-}
-
-.ui.icon.input > i.icon:before,
-.ui.icon.input > i.icon:after {
-  left: 0;
-  position: absolute;
-  text-align: center;
-  top: 50%;
-  width: 100%;
-  margin-top: -0.5em;
-}
-
-.ui.icon.input > i.link.icon {
-  cursor: pointer;
-}
-
-.ui.icon.input > i.circular.icon {
-  top: 0.35em;
-  right: 0.5em;
-}
-
-/* Left Icon Input */
-
-.ui[class*="left icon"].input > i.icon {
-  right: auto;
-  left: 1px;
-  border-radius: 0.28571429rem 0 0 0.28571429rem;
-}
-
-.ui[class*="left icon"].input > i.circular.icon {
-  right: auto;
-  left: 0.5em;
-}
-
-.ui.ui.ui.ui[class*="left icon"].input > textarea,
-.ui.ui.ui.ui[class*="left icon"].input > input {
-  padding-left: 2.67142857em;
-  padding-right: 1em;
-}
-
-/* Focus */
-
-.ui.icon.input > textarea:focus ~ i.icon,
-.ui.icon.input > input:focus ~ i.icon {
-  opacity: 1;
-}
-
-/*--------------------
-          Labeled
-  ---------------------*/
-
-/* Adjacent Label */
-
-.ui.labeled.input > .label {
-  flex: 0 0 auto;
-  margin: 0;
-  font-size: 1em;
-}
-
-.ui.labeled.input > .label:not(.corner) {
-  padding-top: 0.78571429em;
-  padding-bottom: 0.78571429em;
-}
-
-/* Regular Label on Left */
-
-.ui.labeled.input:not([class*="corner labeled"]) .label:first-child {
-  border-top-right-radius: 0;
-  border-bottom-right-radius: 0;
-}
-
-.ui.labeled.input:not([class*="corner labeled"]) .label:first-child + input {
-  border-top-left-radius: 0;
-  border-bottom-left-radius: 0;
-  border-left-color: transparent;
-}
-
-.ui.labeled.input:not([class*="corner labeled"]) .label:first-child + input:focus {
-  border-left-color: #85B7D9;
-}
-
-/* Regular Label on Right */
-
-.ui[class*="right labeled"].input > input {
-  border-top-right-radius: 0 !important;
-  border-bottom-right-radius: 0 !important;
-  border-right-color: transparent !important;
-}
-
-.ui[class*="right labeled"].input > input + .label {
-  border-top-left-radius: 0;
-  border-bottom-left-radius: 0;
-}
-
-.ui[class*="right labeled"].input > input:focus {
-  border-right-color: #85B7D9 !important;
-}
-
-/* Corner Label */
-
-.ui.labeled.input .corner.label {
-  top: 1px;
-  right: 1px;
-  font-size: 0.64285714em;
-  border-radius: 0 0.28571429rem 0 0;
-}
-
-/* Spacing with corner label */
-
-.ui[class*="corner labeled"]:not([class*="left corner labeled"]).labeled.input > textarea,
-.ui[class*="corner labeled"]:not([class*="left corner labeled"]).labeled.input > input {
-  padding-right: 2.5em !important;
-}
-
-.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > textarea,
-.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > input {
-  padding-right: 3.25em !important;
-}
-
-.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > i.icon {
-  margin-right: 1.25em;
-}
-
-/* Left Labeled */
-
-.ui[class*="left corner labeled"].labeled.input > textarea,
-.ui[class*="left corner labeled"].labeled.input > input {
-  padding-left: 2.5em !important;
-}
-
-.ui[class*="left corner labeled"].icon.input > textarea,
-.ui[class*="left corner labeled"].icon.input > input {
-  padding-left: 3.25em !important;
-}
-
-.ui[class*="left corner labeled"].icon.input > i.icon {
-  margin-left: 1.25em;
-}
-
-.ui.icon.input > textarea ~ i.icon {
-  height: 3em;
-}
-
-:not(.field) > .ui.transparent.icon.input > textarea ~ i.icon {
-  height: 1.3em;
-}
-
-/* Corner Label Position  */
-
-.ui.input > .ui.corner.label {
-  top: 1px;
-  right: 1px;
-}
-
-.ui.input > .ui.left.corner.label {
-  right: auto;
-  left: 1px;
-}
-
-/* Labeled and action input states */
-
-.ui.form .field.error > .ui.action.input > .ui.button,
-.ui.form .field.error > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label,
-.ui.action.input.error > .ui.button,
-.ui.labeled.input.error:not([class*="corner labeled"]) > .ui.label {
-  border-top: 1px solid #E0B4B4;
-  border-bottom: 1px solid #E0B4B4;
-}
-
-.ui.form .field.error > .ui[class*="left action"].input > .ui.button,
-.ui.form .field.error > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label,
-.ui[class*="left action"].input.error > .ui.button,
-.ui.labeled.input.error:not(.right):not([class*="corner labeled"]) > .ui.label {
-  border-left: 1px solid #E0B4B4;
-}
-
-.ui.form .field.error > .ui.action.input:not([class*="left action"]) > input + .ui.button,
-.ui.form .field.error > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label,
-.ui.action.input.error:not([class*="left action"]) > input + .ui.button,
-.ui.right.labeled.input.error:not([class*="corner labeled"]) > input + .ui.label {
-  border-right: 1px solid #E0B4B4;
-}
-
-.ui.form .field.error > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child,
-.ui.right.labeled.input.error:not([class*="corner labeled"]) > .ui.label:first-child {
-  border-left: 1px solid #E0B4B4;
-}
-
-.ui.form .field.info > .ui.action.input > .ui.button,
-.ui.form .field.info > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label,
-.ui.action.input.info > .ui.button,
-.ui.labeled.input.info:not([class*="corner labeled"]) > .ui.label {
-  border-top: 1px solid #A9D5DE;
-  border-bottom: 1px solid #A9D5DE;
-}
-
-.ui.form .field.info > .ui[class*="left action"].input > .ui.button,
-.ui.form .field.info > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label,
-.ui[class*="left action"].input.info > .ui.button,
-.ui.labeled.input.info:not(.right):not([class*="corner labeled"]) > .ui.label {
-  border-left: 1px solid #A9D5DE;
-}
-
-.ui.form .field.info > .ui.action.input:not([class*="left action"]) > input + .ui.button,
-.ui.form .field.info > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label,
-.ui.action.input.info:not([class*="left action"]) > input + .ui.button,
-.ui.right.labeled.input.info:not([class*="corner labeled"]) > input + .ui.label {
-  border-right: 1px solid #A9D5DE;
-}
-
-.ui.form .field.info > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child,
-.ui.right.labeled.input.info:not([class*="corner labeled"]) > .ui.label:first-child {
-  border-left: 1px solid #A9D5DE;
-}
-
-.ui.form .field.success > .ui.action.input > .ui.button,
-.ui.form .field.success > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label,
-.ui.action.input.success > .ui.button,
-.ui.labeled.input.success:not([class*="corner labeled"]) > .ui.label {
-  border-top: 1px solid #A3C293;
-  border-bottom: 1px solid #A3C293;
-}
-
-.ui.form .field.success > .ui[class*="left action"].input > .ui.button,
-.ui.form .field.success > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label,
-.ui[class*="left action"].input.success > .ui.button,
-.ui.labeled.input.success:not(.right):not([class*="corner labeled"]) > .ui.label {
-  border-left: 1px solid #A3C293;
-}
-
-.ui.form .field.success > .ui.action.input:not([class*="left action"]) > input + .ui.button,
-.ui.form .field.success > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label,
-.ui.action.input.success:not([class*="left action"]) > input + .ui.button,
-.ui.right.labeled.input.success:not([class*="corner labeled"]) > input + .ui.label {
-  border-right: 1px solid #A3C293;
-}
-
-.ui.form .field.success > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child,
-.ui.right.labeled.input.success:not([class*="corner labeled"]) > .ui.label:first-child {
-  border-left: 1px solid #A3C293;
-}
-
-.ui.form .field.warning > .ui.action.input > .ui.button,
-.ui.form .field.warning > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label,
-.ui.action.input.warning > .ui.button,
-.ui.labeled.input.warning:not([class*="corner labeled"]) > .ui.label {
-  border-top: 1px solid #C9BA9B;
-  border-bottom: 1px solid #C9BA9B;
-}
-
-.ui.form .field.warning > .ui[class*="left action"].input > .ui.button,
-.ui.form .field.warning > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label,
-.ui[class*="left action"].input.warning > .ui.button,
-.ui.labeled.input.warning:not(.right):not([class*="corner labeled"]) > .ui.label {
-  border-left: 1px solid #C9BA9B;
-}
-
-.ui.form .field.warning > .ui.action.input:not([class*="left action"]) > input + .ui.button,
-.ui.form .field.warning > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label,
-.ui.action.input.warning:not([class*="left action"]) > input + .ui.button,
-.ui.right.labeled.input.warning:not([class*="corner labeled"]) > input + .ui.label {
-  border-right: 1px solid #C9BA9B;
-}
-
-.ui.form .field.warning > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child,
-.ui.right.labeled.input.warning:not([class*="corner labeled"]) > .ui.label:first-child {
-  border-left: 1px solid #C9BA9B;
-}
-
-/*--------------------
-          Action
-  ---------------------*/
-
-.ui.action.input > .button,
-.ui.action.input > .buttons {
-  display: flex;
-  align-items: center;
-  flex: 0 0 auto;
-}
-
-.ui.action.input > .button,
-.ui.action.input > .buttons > .button {
-  padding-top: 0.78571429em;
-  padding-bottom: 0.78571429em;
-  margin: 0;
-}
-
-/* Input when ui Left*/
-
-.ui[class*="left action"].input > input {
-  border-top-left-radius: 0;
-  border-bottom-left-radius: 0;
-  border-left-color: transparent;
-}
-
-/* Input when ui Right*/
-
-.ui.action.input:not([class*="left action"]) > input {
-  border-top-right-radius: 0;
-  border-bottom-right-radius: 0;
-  border-right-color: transparent;
-}
-
-/* Button and Dropdown */
-
-.ui.action.input > .dropdown:first-child,
-.ui.action.input > .button:first-child,
-.ui.action.input > .buttons:first-child > .button {
-  border-radius: 0.28571429rem 0 0 0.28571429rem;
-}
-
-.ui.action.input > .dropdown:not(:first-child),
-.ui.action.input > .button:not(:first-child),
-.ui.action.input > .buttons:not(:first-child) > .button {
-  border-radius: 0;
-}
-
-.ui.action.input > .dropdown:last-child,
-.ui.action.input > .button:last-child,
-.ui.action.input > .buttons:last-child > .button {
-  border-radius: 0 0.28571429rem 0.28571429rem 0;
-}
-
-/* Input Focus */
-
-.ui.action.input:not([class*="left action"]) > input:focus {
-  border-right-color: #85B7D9;
-}
-
-.ui.ui[class*="left action"].input > input:focus {
-  border-left-color: #85B7D9;
-}
-
-/*--------------------
-          Fluid
-  ---------------------*/
-
-.ui.fluid.input {
-  display: flex;
-}
-
-.ui.fluid.input > input {
-  width: 0 !important;
-}
-
-/*--------------------
-        Size
----------------------*/
-
-.ui.input {
-  font-size: 1em;
-}
-
-.ui.mini.input {
-  font-size: 0.78571429em;
-}
-
-.ui.tiny.input {
-  font-size: 0.85714286em;
-}
-
-.ui.small.input {
-  font-size: 0.92857143em;
-}
-
-.ui.large.input {
-  font-size: 1.14285714em;
-}
-
-.ui.big.input {
-  font-size: 1.28571429em;
-}
-
-.ui.huge.input {
-  font-size: 1.42857143em;
-}
-
-.ui.massive.input {
-  font-size: 1.71428571em;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Label
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            Label
-*******************************/
-
-.ui.label {
-  display: inline-block;
-  line-height: 1;
-  vertical-align: baseline;
-  margin: 0 0.14285714em;
-  background-color: #E8E8E8;
-  background-image: none;
-  padding: 0.5833em 0.833em;
-  color: rgba(0, 0, 0, 0.6);
-  text-transform: none;
-  font-weight: 500;
-  border: 0 solid transparent;
-  border-radius: 0.28571429rem;
-  transition: background 0.1s ease;
-}
-
-.ui.label:first-child {
-  margin-left: 0;
-}
-
-.ui.label:last-child {
-  margin-right: 0;
-}
-
-/* Link */
-
-a.ui.label {
-  cursor: pointer;
-}
-
-/* Inside Link */
-
-.ui.label > a {
-  cursor: pointer;
-  color: inherit;
-  opacity: 0.5;
-  transition: 0.1s opacity ease;
-}
-
-.ui.label > a:hover {
-  opacity: 1;
-}
-
-/* Image */
-
-.ui.label > img {
-  width: auto !important;
-  vertical-align: middle;
-  height: 2.1666em;
-}
-
-/* Icon */
-
-.ui.left.icon.label > .icon,
-.ui.label > .icon {
-  width: auto;
-  margin: 0 0.75em 0 0;
-}
-
-/* Detail */
-
-.ui.label > .detail {
-  display: inline-block;
-  vertical-align: top;
-  font-weight: 500;
-  margin-left: 1em;
-  opacity: 0.8;
-}
-
-.ui.label > .detail .icon {
-  margin: 0 0.25em 0 0;
-}
-
-/* Removable label */
-
-.ui.label > .close.icon,
-.ui.label > .delete.icon {
-  cursor: pointer;
-  font-size: 0.92857143em;
-  opacity: 0.5;
-  transition: background 0.1s ease;
-}
-
-.ui.label > .close.icon:hover,
-.ui.label > .delete.icon:hover {
-  opacity: 1;
-}
-
-/* Backward compatible positioning */
-
-.ui.label.left.icon > .close.icon,
-.ui.label.left.icon > .delete.icon {
-  margin: 0 0.5em 0 0;
-}
-
-.ui.label:not(.icon) > .close.icon,
-.ui.label:not(.icon) > .delete.icon {
-  margin: 0 0 0 0.5em;
-}
-
-/* Label for only an icon */
-
-.ui.icon.label > .icon {
-  margin: 0 auto;
-}
-
-/* Right Side Icon */
-
-.ui.right.icon.label > .icon {
-  margin: 0 0 0 0.75em;
-}
-
-/*-------------------
-       Group
---------------------*/
-
-.ui.labels > .label {
-  margin: 0 0.5em 0.5em 0;
-}
-
-/*-------------------
-       Coupling
---------------------*/
-
-.ui.header > .ui.label {
-  margin-top: -0.29165em;
-}
-
-/* Remove border radius on attached segment */
-
-.ui.attached.segment > .ui.top.left.attached.label,
-.ui.bottom.attached.segment > .ui.top.left.attached.label {
-  border-top-left-radius: 0;
-}
-
-.ui.attached.segment > .ui.top.right.attached.label,
-.ui.bottom.attached.segment > .ui.top.right.attached.label {
-  border-top-right-radius: 0;
-}
-
-.ui.top.attached.segment > .ui.bottom.left.attached.label {
-  border-bottom-left-radius: 0;
-}
-
-.ui.top.attached.segment > .ui.bottom.right.attached.label {
-  border-bottom-right-radius: 0;
-}
-
-/* Padding on next content after a label */
-
-.ui.top.attached.label ~ .ui.bottom.attached.label + :not(.attached),
-.ui.top.attached.label + :not(.attached) {
-  margin-top: 2rem !important;
-}
-
-.ui.bottom.attached.label ~ :last-child:not(.attached) {
-  margin-top: 0;
-  margin-bottom: 2rem !important;
-}
-
-.ui.segment:not(.basic) > .ui.top.attached.label {
-  margin-top: -1px;
-}
-
-.ui.segment:not(.basic) > .ui.bottom.attached.label {
-  margin-bottom: -1px;
-}
-
-.ui.segment:not(.basic) > .ui.attached.label:not(.right) {
-  margin-left: -1px;
-}
-
-.ui.segment:not(.basic) > .ui.right.attached.label {
-  margin-right: -1px;
-}
-
-.ui.segment:not(.basic) > .ui.attached.label:not(.left):not(.right) {
-  width: calc(100% + 2px);
-}
-
-/*******************************
-             Types
-*******************************/
-
-/*-------------------
-        Attached
-  --------------------*/
-
-.ui[class*="top attached"].label,
-.ui.attached.label {
-  width: 100%;
-  position: absolute;
-  margin: 0;
-  top: 0;
-  left: 0;
-  padding: 0.75em 1em;
-  border-radius: 0.21428571rem 0.21428571rem 0 0;
-}
-
-.ui[class*="bottom attached"].label {
-  top: auto;
-  bottom: 0;
-  border-radius: 0 0 0.21428571rem 0.21428571rem;
-}
-
-.ui[class*="top left attached"].label {
-  width: auto;
-  margin-top: 0;
-  border-radius: 0.21428571rem 0 0.28571429rem 0;
-}
-
-.ui[class*="top right attached"].label {
-  width: auto;
-  left: auto;
-  right: 0;
-  border-radius: 0 0.21428571rem 0 0.28571429rem;
-}
-
-.ui[class*="bottom left attached"].label {
-  width: auto;
-  top: auto;
-  bottom: 0;
-  border-radius: 0 0.28571429rem 0 0.21428571rem;
-}
-
-.ui[class*="bottom right attached"].label {
-  top: auto;
-  bottom: 0;
-  left: auto;
-  right: 0;
-  width: auto;
-  border-radius: 0.28571429rem 0 0.21428571rem 0;
-}
-
-/*******************************
-             States
-*******************************/
-
-/*-------------------
-      Disabled
---------------------*/
-
-.ui.label.disabled {
-  opacity: 0.5;
-}
-
-/*-------------------
-        Hover
---------------------*/
-
-.ui.labels a.label:hover,
-a.ui.label:hover {
-  background-color: #E0E0E0;
-  border-color: #E0E0E0;
-  background-image: none;
-  color: rgba(0, 0, 0, 0.8);
-}
-
-.ui.labels a.label:hover:before,
-a.ui.label:hover:before {
-  color: rgba(0, 0, 0, 0.8);
-}
-
-/*-------------------
-        Active
---------------------*/
-
-.ui.active.label {
-  background-color: #D0D0D0;
-  border-color: #D0D0D0;
-  background-image: none;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.active.label:before {
-  background-color: #D0D0D0;
-  background-image: none;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*-------------------
-     Active Hover
---------------------*/
-
-.ui.labels a.active.label:hover,
-a.ui.active.label:hover {
-  background-color: #C8C8C8;
-  border-color: #C8C8C8;
-  background-image: none;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.labels a.active.label:hover:before,
-a.ui.active.label:hover:before {
-  background-color: #C8C8C8;
-  background-image: none;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*-------------------
-      Visible
---------------------*/
-
-.ui.labels.visible .label,
-.ui.label.visible:not(.dropdown) {
-  display: inline-block !important;
-}
-
-/*-------------------
-      Hidden
---------------------*/
-
-.ui.labels.hidden .label,
-.ui.label.hidden {
-  display: none !important;
-}
-
-/*******************************
-           Variations
-*******************************/
-
-/*-------------------
-          Basic
-  --------------------*/
-
-.ui.basic.labels .label,
-.ui.basic.label {
-  background: none #FFFFFF;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-  color: rgba(0, 0, 0, 0.87);
-  box-shadow: none;
-  padding-top: calc(0.5833em - 1px);
-  padding-bottom: calc(0.5833em - 1px);
-  padding-right: calc(0.833em - 1px);
-}
-
-.ui.basic.labels:not(.tag):not(.image):not(.ribbon) .label,
-.ui.basic.label:not(.tag):not(.image):not(.ribbon) {
-  padding-left: calc(0.833em - 1px);
-}
-
-/* Link */
-
-.ui.basic.labels a.label:hover,
-a.ui.basic.label:hover {
-  text-decoration: none;
-  background: none #FFFFFF;
-  color: #1e70bf;
-  box-shadow: none;
-}
-
-/* Pointing */
-
-.ui.basic.pointing.label:before {
-  border-color: inherit;
-}
-
-/*-------------------
-         Fluid
-  --------------------*/
-
-.ui.label.fluid,
-.ui.fluid.labels > .label {
-  width: 100%;
-  box-sizing: border-box;
-}
-
-/*-------------------
-       Colors
---------------------*/
-
-.ui.primary.labels .label,
-.ui.ui.ui.primary.label {
-  background-color: #2185D0;
-  border-color: #2185D0;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-/* Link */
-
-.ui.primary.labels a.label:hover,
-a.ui.ui.ui.primary.label:hover {
-  background-color: #1678c2;
-  border-color: #1678c2;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .primary.label,
-.ui.ui.ui.basic.primary.label {
-  background: none #FFFFFF;
-  border-color: #2185D0;
-  color: #2185D0;
-}
-
-.ui.basic.labels a.primary.label:hover,
-a.ui.ui.ui.basic.primary.label:hover {
-  background: none #FFFFFF;
-  border-color: #1678c2;
-  color: #1678c2;
-}
-
-.ui.secondary.labels .label,
-.ui.ui.ui.secondary.label {
-  background-color: #1B1C1D;
-  border-color: #1B1C1D;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-/* Link */
-
-.ui.secondary.labels a.label:hover,
-a.ui.ui.ui.secondary.label:hover {
-  background-color: #27292a;
-  border-color: #27292a;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .secondary.label,
-.ui.ui.ui.basic.secondary.label {
-  background: none #FFFFFF;
-  border-color: #1B1C1D;
-  color: #1B1C1D;
-}
-
-.ui.basic.labels a.secondary.label:hover,
-a.ui.ui.ui.basic.secondary.label:hover {
-  background: none #FFFFFF;
-  border-color: #27292a;
-  color: #27292a;
-}
-
-.ui.red.labels .label,
-.ui.ui.ui.red.label {
-  background-color: #DB2828;
-  border-color: #DB2828;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.red.labels a.label:hover,
-a.ui.ui.ui.red.label:hover {
-  background-color: #d01919;
-  border-color: #d01919;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .red.label,
-.ui.ui.ui.basic.red.label {
-  background: none #FFFFFF;
-  border-color: #DB2828;
-  color: #DB2828;
-}
-
-.ui.basic.labels a.red.label:hover,
-a.ui.ui.ui.basic.red.label:hover {
-  background: none #FFFFFF;
-  border-color: #d01919;
-  color: #d01919;
-}
-
-.ui.orange.labels .label,
-.ui.ui.ui.orange.label {
-  background-color: #F2711C;
-  border-color: #F2711C;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.orange.labels a.label:hover,
-a.ui.ui.ui.orange.label:hover {
-  background-color: #f26202;
-  border-color: #f26202;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .orange.label,
-.ui.ui.ui.basic.orange.label {
-  background: none #FFFFFF;
-  border-color: #F2711C;
-  color: #F2711C;
-}
-
-.ui.basic.labels a.orange.label:hover,
-a.ui.ui.ui.basic.orange.label:hover {
-  background: none #FFFFFF;
-  border-color: #f26202;
-  color: #f26202;
-}
-
-.ui.yellow.labels .label,
-.ui.ui.ui.yellow.label {
-  background-color: #FBBD08;
-  border-color: #FBBD08;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.yellow.labels a.label:hover,
-a.ui.ui.ui.yellow.label:hover {
-  background-color: #eaae00;
-  border-color: #eaae00;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .yellow.label,
-.ui.ui.ui.basic.yellow.label {
-  background: none #FFFFFF;
-  border-color: #FBBD08;
-  color: #FBBD08;
-}
-
-.ui.basic.labels a.yellow.label:hover,
-a.ui.ui.ui.basic.yellow.label:hover {
-  background: none #FFFFFF;
-  border-color: #eaae00;
-  color: #eaae00;
-}
-
-.ui.olive.labels .label,
-.ui.ui.ui.olive.label {
-  background-color: #B5CC18;
-  border-color: #B5CC18;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.olive.labels a.label:hover,
-a.ui.ui.ui.olive.label:hover {
-  background-color: #a7bd0d;
-  border-color: #a7bd0d;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .olive.label,
-.ui.ui.ui.basic.olive.label {
-  background: none #FFFFFF;
-  border-color: #B5CC18;
-  color: #B5CC18;
-}
-
-.ui.basic.labels a.olive.label:hover,
-a.ui.ui.ui.basic.olive.label:hover {
-  background: none #FFFFFF;
-  border-color: #a7bd0d;
-  color: #a7bd0d;
-}
-
-.ui.green.labels .label,
-.ui.ui.ui.green.label {
-  background-color: #21BA45;
-  border-color: #21BA45;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.green.labels a.label:hover,
-a.ui.ui.ui.green.label:hover {
-  background-color: #16ab39;
-  border-color: #16ab39;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .green.label,
-.ui.ui.ui.basic.green.label {
-  background: none #FFFFFF;
-  border-color: #21BA45;
-  color: #21BA45;
-}
-
-.ui.basic.labels a.green.label:hover,
-a.ui.ui.ui.basic.green.label:hover {
-  background: none #FFFFFF;
-  border-color: #16ab39;
-  color: #16ab39;
-}
-
-.ui.teal.labels .label,
-.ui.ui.ui.teal.label {
-  background-color: #00B5AD;
-  border-color: #00B5AD;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.teal.labels a.label:hover,
-a.ui.ui.ui.teal.label:hover {
-  background-color: #009c95;
-  border-color: #009c95;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .teal.label,
-.ui.ui.ui.basic.teal.label {
-  background: none #FFFFFF;
-  border-color: #00B5AD;
-  color: #00B5AD;
-}
-
-.ui.basic.labels a.teal.label:hover,
-a.ui.ui.ui.basic.teal.label:hover {
-  background: none #FFFFFF;
-  border-color: #009c95;
-  color: #009c95;
-}
-
-.ui.blue.labels .label,
-.ui.ui.ui.blue.label {
-  background-color: #2185D0;
-  border-color: #2185D0;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.blue.labels a.label:hover,
-a.ui.ui.ui.blue.label:hover {
-  background-color: #1678c2;
-  border-color: #1678c2;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .blue.label,
-.ui.ui.ui.basic.blue.label {
-  background: none #FFFFFF;
-  border-color: #2185D0;
-  color: #2185D0;
-}
-
-.ui.basic.labels a.blue.label:hover,
-a.ui.ui.ui.basic.blue.label:hover {
-  background: none #FFFFFF;
-  border-color: #1678c2;
-  color: #1678c2;
-}
-
-.ui.violet.labels .label,
-.ui.ui.ui.violet.label {
-  background-color: #6435C9;
-  border-color: #6435C9;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.violet.labels a.label:hover,
-a.ui.ui.ui.violet.label:hover {
-  background-color: #5829bb;
-  border-color: #5829bb;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .violet.label,
-.ui.ui.ui.basic.violet.label {
-  background: none #FFFFFF;
-  border-color: #6435C9;
-  color: #6435C9;
-}
-
-.ui.basic.labels a.violet.label:hover,
-a.ui.ui.ui.basic.violet.label:hover {
-  background: none #FFFFFF;
-  border-color: #5829bb;
-  color: #5829bb;
-}
-
-.ui.purple.labels .label,
-.ui.ui.ui.purple.label {
-  background-color: #A333C8;
-  border-color: #A333C8;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.purple.labels a.label:hover,
-a.ui.ui.ui.purple.label:hover {
-  background-color: #9627ba;
-  border-color: #9627ba;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .purple.label,
-.ui.ui.ui.basic.purple.label {
-  background: none #FFFFFF;
-  border-color: #A333C8;
-  color: #A333C8;
-}
-
-.ui.basic.labels a.purple.label:hover,
-a.ui.ui.ui.basic.purple.label:hover {
-  background: none #FFFFFF;
-  border-color: #9627ba;
-  color: #9627ba;
-}
-
-.ui.pink.labels .label,
-.ui.ui.ui.pink.label {
-  background-color: #E03997;
-  border-color: #E03997;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.pink.labels a.label:hover,
-a.ui.ui.ui.pink.label:hover {
-  background-color: #e61a8d;
-  border-color: #e61a8d;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .pink.label,
-.ui.ui.ui.basic.pink.label {
-  background: none #FFFFFF;
-  border-color: #E03997;
-  color: #E03997;
-}
-
-.ui.basic.labels a.pink.label:hover,
-a.ui.ui.ui.basic.pink.label:hover {
-  background: none #FFFFFF;
-  border-color: #e61a8d;
-  color: #e61a8d;
-}
-
-.ui.brown.labels .label,
-.ui.ui.ui.brown.label {
-  background-color: #A5673F;
-  border-color: #A5673F;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.brown.labels a.label:hover,
-a.ui.ui.ui.brown.label:hover {
-  background-color: #975b33;
-  border-color: #975b33;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .brown.label,
-.ui.ui.ui.basic.brown.label {
-  background: none #FFFFFF;
-  border-color: #A5673F;
-  color: #A5673F;
-}
-
-.ui.basic.labels a.brown.label:hover,
-a.ui.ui.ui.basic.brown.label:hover {
-  background: none #FFFFFF;
-  border-color: #975b33;
-  color: #975b33;
-}
-
-.ui.grey.labels .label,
-.ui.ui.ui.grey.label {
-  background-color: #767676;
-  border-color: #767676;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.grey.labels a.label:hover,
-a.ui.ui.ui.grey.label:hover {
-  background-color: #838383;
-  border-color: #838383;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .grey.label,
-.ui.ui.ui.basic.grey.label {
-  background: none #FFFFFF;
-  border-color: #767676;
-  color: #767676;
-}
-
-.ui.basic.labels a.grey.label:hover,
-a.ui.ui.ui.basic.grey.label:hover {
-  background: none #FFFFFF;
-  border-color: #838383;
-  color: #838383;
-}
-
-.ui.black.labels .label,
-.ui.ui.ui.black.label {
-  background-color: #1B1C1D;
-  border-color: #1B1C1D;
-  color: #FFFFFF;
-}
-
-/* Link */
-
-.ui.black.labels a.label:hover,
-a.ui.ui.ui.black.label:hover {
-  background-color: #27292a;
-  border-color: #27292a;
-  color: #FFFFFF;
-}
-
-/* Basic */
-
-.ui.basic.labels .black.label,
-.ui.ui.ui.basic.black.label {
-  background: none #FFFFFF;
-  border-color: #1B1C1D;
-  color: #1B1C1D;
-}
-
-.ui.basic.labels a.black.label:hover,
-a.ui.ui.ui.basic.black.label:hover {
-  background: none #FFFFFF;
-  border-color: #27292a;
-  color: #27292a;
-}
-
-/*-------------------
-     Horizontal
---------------------*/
-
-.ui.horizontal.labels .label,
-.ui.horizontal.label {
-  margin: 0 0.5em 0 0;
-  padding: 0.4em 0.833em;
-  min-width: 3em;
-  text-align: center;
-}
-
-/*-------------------
-         Circular
-  --------------------*/
-
-.ui.circular.labels .label,
-.ui.circular.label {
-  min-width: 2em;
-  min-height: 2em;
-  padding: 0.5em !important;
-  line-height: 1em;
-  text-align: center;
-  border-radius: 500rem;
-}
-
-.ui.empty.circular.labels .label,
-.ui.empty.circular.label {
-  min-width: 0;
-  min-height: 0;
-  overflow: hidden;
-  width: 0.5em;
-  height: 0.5em;
-  vertical-align: baseline;
-}
-
-/*-------------------
-         Pointing
-  --------------------*/
-
-.ui.pointing.label {
-  position: relative;
-}
-
-.ui.attached.pointing.label {
-  position: absolute;
-}
-
-.ui.pointing.label:before {
-  background-color: inherit;
-  background-image: inherit;
-  border-width: 0;
-  border-style: solid;
-  border-color: inherit;
-}
-
-/* Arrow */
-
-.ui.pointing.label:before {
-  position: absolute;
-  content: '';
-  transform: rotate(45deg);
-  background-image: none;
-  z-index: 2;
-  width: 0.6666em;
-  height: 0.6666em;
-  transition: none;
-}
-
-/*--- Above ---*/
-
-.ui.pointing.label,
-.ui[class*="pointing above"].label {
-  margin-top: 1em;
-}
-
-.ui.pointing.label:before,
-.ui[class*="pointing above"].label:before {
-  border-width: 1px 0 0 1px;
-  transform: translateX(-50%) translateY(-50%) rotate(45deg);
-  top: 0;
-  left: 50%;
-}
-
-/*--- Below ---*/
-
-.ui[class*="bottom pointing"].label,
-.ui[class*="pointing below"].label {
-  margin-top: 0;
-  margin-bottom: 1em;
-}
-
-.ui[class*="bottom pointing"].label:before,
-.ui[class*="pointing below"].label:before {
-  border-width: 0 1px 1px 0;
-  top: auto;
-  right: auto;
-  transform: translateX(-50%) translateY(-50%) rotate(45deg);
-  top: 100%;
-  left: 50%;
-}
-
-/*--- Left ---*/
-
-.ui[class*="left pointing"].label {
-  margin-top: 0;
-  margin-left: 0.6666em;
-}
-
-.ui[class*="left pointing"].label:before {
-  border-width: 0 0 1px 1px;
-  transform: translateX(-50%) translateY(-50%) rotate(45deg);
-  bottom: auto;
-  right: auto;
-  top: 50%;
-  left: 0;
-}
-
-/*--- Right ---*/
-
-.ui[class*="right pointing"].label {
-  margin-top: 0;
-  margin-right: 0.6666em;
-}
-
-.ui[class*="right pointing"].label:before {
-  border-width: 1px 1px 0 0;
-  transform: translateX(50%) translateY(-50%) rotate(45deg);
-  top: 50%;
-  right: 0;
-  bottom: auto;
-  left: auto;
-}
-
-/* Basic Pointing */
-
-/*--- Above ---*/
-
-.ui.basic.pointing.label:before,
-.ui.basic[class*="pointing above"].label:before {
-  margin-top: -1px;
-}
-
-/*--- Below ---*/
-
-.ui.basic[class*="bottom pointing"].label:before,
-.ui.basic[class*="pointing below"].label:before {
-  bottom: auto;
-  top: 100%;
-  margin-top: 1px;
-}
-
-/*--- Left ---*/
-
-.ui.basic[class*="left pointing"].label:before {
-  top: 50%;
-  left: -1px;
-}
-
-/*--- Right ---*/
-
-.ui.basic[class*="right pointing"].label:before {
-  top: 50%;
-  right: -1px;
-}
-
-/*------------------
-     Floating Label
-  -------------------*/
-
-.ui.floating.label {
-  position: absolute;
-  z-index: 100;
-  top: -1em;
-  right: 0;
-  white-space: nowrap;
-  transform: translateX(50%);
-}
-
-.ui.right.aligned.floating.label {
-  transform: translateX(1.2em);
-}
-
-.ui.left.floating.label {
-  left: 0;
-  right: auto;
-  transform: translateX(-50%);
-}
-
-.ui.left.aligned.floating.label {
-  transform: translateX(-1.2em);
-}
-
-.ui.bottom.floating.label {
-  top: auto;
-  bottom: -1em;
-}
-
-/*-------------------
-        Sizes
---------------------*/
-
-.ui.labels .label,
-.ui.label {
-  font-size: 0.85714286rem;
-}
-
-.ui.mini.labels .label,
-.ui.mini.label {
-  font-size: 0.64285714rem;
-}
-
-.ui.tiny.labels .label,
-.ui.tiny.label {
-  font-size: 0.71428571rem;
-}
-
-.ui.small.labels .label,
-.ui.small.label {
-  font-size: 0.78571429rem;
-}
-
-.ui.large.labels .label,
-.ui.large.label {
-  font-size: 1rem;
-}
-
-.ui.big.labels .label,
-.ui.big.label {
-  font-size: 1.28571429rem;
-}
-
-.ui.huge.labels .label,
-.ui.huge.label {
-  font-size: 1.42857143rem;
-}
-
-.ui.massive.labels .label,
-.ui.massive.label {
-  font-size: 1.71428571rem;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - List
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            List
-*******************************/
-
-ul.ui.list,
-ol.ui.list,
-.ui.list {
-  list-style-type: none;
-  margin: 1em 0;
-  padding: 0 0;
-}
-
-ul.ui.list:first-child,
-ol.ui.list:first-child,
-.ui.list:first-child {
-  margin-top: 0;
-  padding-top: 0;
-}
-
-ul.ui.list:last-child,
-ol.ui.list:last-child,
-.ui.list:last-child {
-  margin-bottom: 0;
-  padding-bottom: 0;
-}
-
-/*******************************
-            Content
-*******************************/
-
-/* List Item */
-
-ul.ui.list li,
-ol.ui.list li,
-.ui.list > .item,
-.ui.list .list > .item {
-  display: list-item;
-  table-layout: fixed;
-  list-style-type: none;
-  list-style-position: outside;
-  padding: 0.21428571em 0;
-  line-height: 1.14285714em;
-}
-
-ul.ui.list > li:first-child:after,
-ol.ui.list > li:first-child:after,
-.ui.list > .list > .item:after,
-.ui.list > .item:after {
-  content: '';
-  display: block;
-  height: 0;
-  clear: both;
-  visibility: hidden;
-}
-
-ul.ui.list li:first-child,
-ol.ui.list li:first-child,
-.ui.list .list > .item:first-child,
-.ui.list > .item:first-child {
-  padding-top: 0;
-}
-
-ul.ui.list li:last-child,
-ol.ui.list li:last-child,
-.ui.list .list > .item:last-child,
-.ui.list > .item:last-child {
-  padding-bottom: 0;
-}
-
-/* Child List */
-
-ul.ui.list ul,
-ol.ui.list ol,
-.ui.list .list:not(.icon) {
-  clear: both;
-  margin: 0;
-  padding: 0.75em 0 0.25em 0.5em;
-}
-
-/* Child Item */
-
-ul.ui.list ul li,
-ol.ui.list ol li,
-.ui.list .list > .item {
-  padding: 0.14285714em 0;
-  line-height: inherit;
-}
-
-/* Icon */
-
-.ui.list .list > .item > i.icon,
-.ui.list > .item > i.icon {
-  display: table-cell;
-  min-width: 1.55em;
-  margin: 0;
-  padding-top: 0;
-  transition: color 0.1s ease;
-}
-
-.ui.list .list > .item > i.icon:not(.loading),
-.ui.list > .item > i.icon:not(.loading) {
-  padding-right: 0.28571429em;
-  vertical-align: top;
-}
-
-.ui.list .list > .item > i.icon:only-child,
-.ui.list > .item > i.icon:only-child {
-  display: inline-block;
-  min-width: auto;
-  vertical-align: top;
-}
-
-/* Image */
-
-.ui.list .list > .item > .image,
-.ui.list > .item > .image {
-  display: table-cell;
-  background-color: transparent;
-  margin: 0;
-  vertical-align: top;
-}
-
-.ui.list .list > .item > .image:not(:only-child):not(img),
-.ui.list > .item > .image:not(:only-child):not(img) {
-  padding-right: 0.5em;
-}
-
-.ui.list .list > .item > .image img,
-.ui.list > .item > .image img {
-  vertical-align: top;
-}
-
-.ui.list .list > .item > img.image,
-.ui.list .list > .item > .image:only-child,
-.ui.list > .item > img.image,
-.ui.list > .item > .image:only-child {
-  display: inline-block;
-}
-
-/* Content */
-
-.ui.list .list > .item > .content,
-.ui.list > .item > .content {
-  line-height: 1.14285714em;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-.ui.list .list > .item > .image + .content,
-.ui.list .list > .item > i.icon + .content,
-.ui.list > .item > .image + .content,
-.ui.list > .item > i.icon + .content {
-  display: table-cell;
-  width: 100%;
-  padding: 0 0 0 0.5em;
-  vertical-align: top;
-}
-
-.ui.list .list > .item > i.loading.icon + .content,
-.ui.list > .item > i.loading.icon + .content {
-  padding-left: calc(0.2857142857142857em + 0.5em);
-}
-
-.ui.list .list > .item > img.image + .content,
-.ui.list > .item > img.image + .content {
-  display: inline-block;
-  width: auto;
-}
-
-.ui.list .list > .item > .content > .list,
-.ui.list > .item > .content > .list {
-  margin-left: 0;
-  padding-left: 0;
-}
-
-/* Header */
-
-.ui.list .list > .item .header,
-.ui.list > .item .header {
-  display: block;
-  margin: 0;
-  font-family: var(--fonts-regular);
-  font-weight: 500;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-/* Description */
-
-.ui.list .list > .item .description,
-.ui.list > .item .description {
-  display: block;
-  color: rgba(0, 0, 0, 0.7);
-}
-
-/* Child Link */
-
-.ui.list > .item a,
-.ui.list .list > .item a {
-  cursor: pointer;
-}
-
-/* Linking Item */
-
-.ui.list .list > a.item,
-.ui.list > a.item {
-  cursor: pointer;
-  color: #4183C4;
-}
-
-.ui.list .list > a.item:hover,
-.ui.list > a.item:hover {
-  color: #1e70bf;
-}
-
-/* Linked Item Icons */
-
-.ui.list .list > a.item > i.icons,
-.ui.list > a.item > i.icons,
-.ui.list .list > a.item > i.icon,
-.ui.list > a.item > i.icon {
-  color: rgba(0, 0, 0, 0.4);
-}
-
-/* Header Link */
-
-.ui.list .list > .item a.header,
-.ui.list > .item a.header {
-  cursor: pointer;
-  color: #4183C4 !important;
-}
-
-.ui.list .list > .item > a.header:hover,
-.ui.list > .item > a.header:hover {
-  color: #1e70bf !important;
-}
-
-/* Floated Content */
-
-.ui[class*="left floated"].list {
-  float: left;
-}
-
-.ui[class*="right floated"].list {
-  float: right;
-}
-
-.ui.list .list > .item [class*="left floated"],
-.ui.list > .item [class*="left floated"] {
-  float: left;
-  margin: 0 1em 0 0;
-}
-
-.ui.list .list > .item [class*="right floated"],
-.ui.list > .item [class*="right floated"] {
-  float: right;
-  margin: 0 0 0 1em;
-}
-
-/*******************************
-            Coupling
-*******************************/
-
-.ui.menu .ui.list > .item,
-.ui.menu .ui.list .list > .item {
-  display: list-item;
-  table-layout: fixed;
-  background-color: transparent;
-  list-style-type: none;
-  list-style-position: outside;
-  padding: 0.21428571em 0;
-  line-height: 1.14285714em;
-}
-
-.ui.menu .ui.list .list > .item:before,
-.ui.menu .ui.list > .item:before {
-  border: none;
-  background: none;
-}
-
-.ui.menu .ui.list .list > .item:first-child,
-.ui.menu .ui.list > .item:first-child {
-  padding-top: 0;
-}
-
-.ui.menu .ui.list .list > .item:last-child,
-.ui.menu .ui.list > .item:last-child {
-  padding-bottom: 0;
-}
-
-/*******************************
-            Types
-*******************************/
-
-/*-------------------
-        Horizontal
-  --------------------*/
-
-.ui.horizontal.list {
-  display: inline-block;
-  font-size: 0;
-}
-
-.ui.horizontal.list > .item {
-  display: inline-block;
-  margin-right: 1em;
-  font-size: 1rem;
-}
-
-.ui.horizontal.list:not(.celled) > .item:last-child {
-  margin-right: 0;
-  padding-right: 0;
-}
-
-.ui.horizontal.list .list:not(.icon) {
-  padding-left: 0;
-  padding-bottom: 0;
-}
-
-.ui.horizontal.list > .item > .image,
-.ui.horizontal.list .list > .item > .image,
-.ui.horizontal.list > .item > i.icon,
-.ui.horizontal.list .list > .item > i.icon,
-.ui.horizontal.list > .item > .content,
-.ui.horizontal.list .list > .item > .content {
-  vertical-align: middle;
-}
-
-/* Padding on all elements */
-
-.ui.horizontal.list > .item:first-child,
-.ui.horizontal.list > .item:last-child {
-  padding-top: 0.21428571em;
-  padding-bottom: 0.21428571em;
-}
-
-/* Horizontal List */
-
-.ui.horizontal.list > .item > i.icon,
-.ui.horizontal.list .item > i.icons > i.icon {
-  margin: 0;
-  padding: 0 0.25em 0 0;
-}
-
-.ui.horizontal.list > .item > .image + .content,
-.ui.horizontal.list > .item > i.icon,
-.ui.horizontal.list > .item > i.icon + .content {
-  float: none;
-  display: inline-block;
-  width: auto;
-}
-
-.ui.horizontal.list > .item > .image {
-  display: inline-block;
-}
-
-/*******************************
-             States
-*******************************/
-
-/*-------------------
-         Disabled
-  --------------------*/
-
-.ui.list .list > .disabled.item,
-.ui.list > .disabled.item {
-  pointer-events: none;
-  color: rgba(40, 40, 40, 0.3) !important;
-}
-
-/*-------------------
-        Hover
---------------------*/
-
-.ui.list .list > a.item:hover > .icons,
-.ui.list > a.item:hover > .icons,
-.ui.list .list > a.item:hover > i.icon,
-.ui.list > a.item:hover > i.icon {
-  color: rgba(0, 0, 0, 0.87);
-}
-
-/*******************************
-           Variations
-*******************************/
-
-/*-------------------
-         Aligned
-  --------------------*/
-
-.ui.list[class*="top aligned"] .image,
-.ui.list[class*="top aligned"] .content,
-.ui.list [class*="top aligned"] {
-  vertical-align: top !important;
-}
-
-.ui.list[class*="middle aligned"] .image,
-.ui.list[class*="middle aligned"] .content,
-.ui.list [class*="middle aligned"] {
-  vertical-align: middle !important;
-}
-
-.ui.list[class*="bottom aligned"] .image,
-.ui.list[class*="bottom aligned"] .content,
-.ui.list [class*="bottom aligned"] {
-  vertical-align: bottom !important;
-}
-
-/*-------------------
-         Link
-  --------------------*/
-
-.ui.link.list .item,
-.ui.link.list a.item,
-.ui.link.list .item a:not(.ui) {
-  color: rgba(0, 0, 0, 0.4);
-  transition: 0.1s color ease;
-}
-
-.ui.link.list.list a.item:hover,
-.ui.link.list.list .item a:not(.ui):hover {
-  color: rgba(0, 0, 0, 0.8);
-}
-
-.ui.link.list.list a.item:active,
-.ui.link.list.list .item a:not(.ui):active {
-  color: rgba(0, 0, 0, 0.9);
-}
-
-.ui.link.list.list .active.item,
-.ui.link.list.list .active.item a:not(.ui) {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*-------------------
-        Selection
-  --------------------*/
-
-.ui.selection.list .list > .item,
-.ui.selection.list > .item {
-  cursor: pointer;
-  background: transparent;
-  padding: 0.5em 0.5em;
-  margin: 0;
-  color: rgba(0, 0, 0, 0.4);
-  border-radius: 0.5em;
-  transition: 0.1s color ease, 0.1s padding-left ease, 0.1s background-color ease;
-}
-
-.ui.selection.list .list > .item:last-child,
-.ui.selection.list > .item:last-child {
-  margin-bottom: 0;
-}
-
-.ui.selection.list .list > .item:hover,
-.ui.selection.list > .item:hover {
-  background: rgba(0, 0, 0, 0.03);
-  color: rgba(0, 0, 0, 0.8);
-}
-
-.ui.selection.list .list > .item:active,
-.ui.selection.list > .item:active {
-  background: rgba(0, 0, 0, 0.05);
-  color: rgba(0, 0, 0, 0.9);
-}
-
-.ui.selection.list .list > .item.active,
-.ui.selection.list > .item.active {
-  background: rgba(0, 0, 0, 0.05);
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/* Celled / Divided Selection List */
-
-.ui.celled.selection.list .list > .item,
-.ui.divided.selection.list .list > .item,
-.ui.celled.selection.list > .item,
-.ui.divided.selection.list > .item {
-  border-radius: 0;
-}
-
-/*-------------------
-         Animated
-  --------------------*/
-
-.ui.animated.list > .item {
-  transition: 0.25s color ease 0.1s, 0.25s padding-left ease 0.1s, 0.25s background-color ease 0.1s;
-}
-
-.ui.animated.list:not(.horizontal) > .item:hover {
-  padding-left: 1em;
-}
-
-/*-------------------
-         Fitted
-  --------------------*/
-
-.ui.fitted.list:not(.selection) .list > .item,
-.ui.fitted.list:not(.selection) > .item {
-  padding-left: 0;
-  padding-right: 0;
-}
-
-.ui.fitted.selection.list .list > .item,
-.ui.fitted.selection.list > .item {
-  margin-left: -0.5em;
-  margin-right: -0.5em;
-}
-
-/*-------------------
-        Bulleted
-  --------------------*/
-
-ul.ui.list,
-.ui.bulleted.list {
-  margin-left: 1.25rem;
-}
-
-ul.ui.list li,
-.ui.bulleted.list .list > .item,
-.ui.bulleted.list > .item {
-  position: relative;
-}
-
-ul.ui.list li:before,
-.ui.bulleted.list .list > .item:before,
-.ui.bulleted.list > .item:before {
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  user-select: none;
-  pointer-events: none;
-  position: absolute;
-  top: auto;
-  left: auto;
-  font-weight: normal;
-  margin-left: -1.25rem;
-  content: '\2022';
-  opacity: 1;
-  color: inherit;
-  vertical-align: top;
-}
-
-ul.ui.list li:before,
-.ui.bulleted.list .list > a.item:before,
-.ui.bulleted.list > a.item:before {
-  color: rgba(0, 0, 0, 0.87);
-}
-
-ul.ui.list ul,
-.ui.bulleted.list .list:not(.icon) {
-  padding-left: 1.25rem;
-}
-
-/* Horizontal Bulleted */
-
-ul.ui.horizontal.bulleted.list,
-.ui.horizontal.bulleted.list {
-  margin-left: 0;
-}
-
-ul.ui.horizontal.bulleted.list li,
-.ui.horizontal.bulleted.list > .item {
-  margin-left: 1.75rem;
-}
-
-ul.ui.horizontal.bulleted.list li:first-child,
-.ui.horizontal.bulleted.list > .item:first-child {
-  margin-left: 0;
-}
-
-ul.ui.horizontal.bulleted.list li::before,
-.ui.horizontal.bulleted.list > .item::before {
-  color: rgba(0, 0, 0, 0.87);
-}
-
-ul.ui.horizontal.bulleted.list li:first-child::before,
-.ui.horizontal.bulleted.list > .item:first-child::before {
-  display: none;
-}
-
-/*-------------------
-         Ordered
-  --------------------*/
-
-ol.ui.list,
-.ui.ordered.list,
-.ui.ordered.list .list:not(.icon),
-ol.ui.list ol {
-  counter-reset: ordered;
-  margin-left: 1.25rem;
-  list-style-type: none;
-}
-
-ol.ui.list li,
-.ui.ordered.list .list > .item,
-.ui.ordered.list > .item {
-  list-style-type: none;
-  position: relative;
-}
-
-ol.ui.list li:before,
-.ui.ordered.list .list > .item:before,
-.ui.ordered.list > .item:before {
-  position: absolute;
-  top: auto;
-  left: auto;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  user-select: none;
-  pointer-events: none;
-  margin-left: -1.25rem;
-  counter-increment: ordered;
-  content: counters(ordered, ".") " ";
-  text-align: right;
-  color: rgba(0, 0, 0, 0.87);
-  vertical-align: middle;
-  opacity: 0.8;
-}
-
-/* Value */
-
-.ui.ordered.list .list > .item[data-value]:before,
-.ui.ordered.list > .item[data-value]:before {
-  content: attr(data-value);
-}
-
-ol.ui.list li[value]:before {
-  content: attr(value);
-}
-
-/* Child Lists */
-
-ol.ui.list ol,
-.ui.ordered.list .list:not(.icon) {
-  margin-left: 1em;
-}
-
-ol.ui.list ol li:before,
-.ui.ordered.list .list > .item:before {
-  margin-left: -2em;
-}
-
-/* Horizontal Ordered */
-
-ol.ui.horizontal.list,
-.ui.ordered.horizontal.list {
-  margin-left: 0;
-}
-
-ol.ui.horizontal.list li:before,
-.ui.ordered.horizontal.list .list > .item:before,
-.ui.ordered.horizontal.list > .item:before {
-  position: static;
-  margin: 0 0.5em 0 0;
-}
-
-/* Suffixed Ordered */
-
-ol.ui.suffixed.list li:before,
-.ui.suffixed.ordered.list .list > .item:before,
-.ui.suffixed.ordered.list > .item:before {
-  content: counters(ordered, ".") ".";
-}
-
-/*-------------------
-         Divided
-  --------------------*/
-
-.ui.divided.list > .item {
-  border-top: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.divided.list .list > .item {
-  border-top: none;
-}
-
-.ui.divided.list .item .list > .item {
-  border-top: none;
-}
-
-.ui.divided.list .list > .item:first-child,
-.ui.divided.list > .item:first-child {
-  border-top: none;
-}
-
-/* Sub Menu */
-
-.ui.divided.list:not(.horizontal) .list > .item:first-child {
-  border-top-width: 1px;
-}
-
-/* Divided bulleted */
-
-.ui.divided.bulleted.list:not(.horizontal),
-.ui.divided.bulleted.list .list:not(.icon) {
-  margin-left: 0;
-  padding-left: 0;
-}
-
-.ui.divided.bulleted.list > .item:not(.horizontal) {
-  padding-left: 1.25rem;
-}
-
-/* Divided Ordered */
-
-.ui.divided.ordered.list {
-  margin-left: 0;
-}
-
-.ui.divided.ordered.list .list > .item,
-.ui.divided.ordered.list > .item {
-  padding-left: 1.25rem;
-}
-
-.ui.divided.ordered.list .item .list:not(.icon) {
-  margin-left: 0;
-  margin-right: 0;
-  padding-bottom: 0.21428571em;
-}
-
-.ui.divided.ordered.list .item .list > .item {
-  padding-left: 1em;
-}
-
-/* Divided Selection */
-
-.ui.divided.selection.list .list > .item,
-.ui.divided.selection.list > .item {
-  margin: 0;
-  border-radius: 0;
-}
-
-/* Divided horizontal */
-
-.ui.divided.horizontal.list {
-  margin-left: 0;
-}
-
-.ui.divided.horizontal.list > .item {
-  padding-left: 0.5em;
-}
-
-.ui.divided.horizontal.list > .item:not(:last-child) {
-  padding-right: 0.5em;
-}
-
-.ui.divided.horizontal.list > .item {
-  border-top: none;
-  border-right: 1px solid rgba(34, 36, 38, 0.15);
-  margin: 0;
-  line-height: 0.6;
-}
-
-.ui.horizontal.divided.list > .item:last-child {
-  border-right: none;
-}
-
-/*-------------------
-          Celled
-  --------------------*/
-
-.ui.celled.list > .item,
-.ui.celled.list > .list {
-  border-top: 1px solid rgba(34, 36, 38, 0.15);
-  padding-left: 0.5em;
-  padding-right: 0.5em;
-}
-
-.ui.celled.list > .item:last-child {
-  border-bottom: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-/* Padding on all elements */
-
-.ui.celled.list > .item:first-child,
-.ui.celled.list > .item:last-child {
-  padding-top: 0.21428571em;
-  padding-bottom: 0.21428571em;
-}
-
-/* Sub Menu */
-
-.ui.celled.list .item .list > .item {
-  border-width: 0;
-}
-
-.ui.celled.list .list > .item:first-child {
-  border-top-width: 0;
-}
-
-/* Celled Bulleted */
-
-.ui.celled.bulleted.list {
-  margin-left: 0;
-}
-
-.ui.celled.bulleted.list .list > .item,
-.ui.celled.bulleted.list > .item {
-  padding-left: 1.25rem;
-}
-
-.ui.celled.bulleted.list .item .list:not(.icon) {
-  margin-left: -1.25rem;
-  margin-right: -1.25rem;
-  padding-bottom: 0.21428571em;
-}
-
-/* Celled Ordered */
-
-.ui.celled.ordered.list {
-  margin-left: 0;
-}
-
-.ui.celled.ordered.list .list > .item,
-.ui.celled.ordered.list > .item {
-  padding-left: 1.25rem;
-}
-
-.ui.celled.ordered.list .item .list:not(.icon) {
-  margin-left: 0;
-  margin-right: 0;
-  padding-bottom: 0.21428571em;
-}
-
-.ui.celled.ordered.list .list > .item {
-  padding-left: 1em;
-}
-
-/* Celled Horizontal */
-
-.ui.horizontal.celled.list {
-  margin-left: 0;
-}
-
-.ui.horizontal.celled.list .list > .item,
-.ui.horizontal.celled.list > .item {
-  border-top: none;
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-  margin: 0;
-  padding-left: 0.5em;
-  padding-right: 0.5em;
-  line-height: 0.6;
-}
-
-.ui.horizontal.celled.list .list > .item:last-child,
-.ui.horizontal.celled.list > .item:last-child {
-  border-bottom: none;
-  border-right: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-/*-------------------
-         Relaxed
-  --------------------*/
-
-.ui.relaxed.list:not(.horizontal) > .item:not(:first-child) {
-  padding-top: 0.42857143em;
-}
-
-.ui.relaxed.list:not(.horizontal) > .item:not(:last-child) {
-  padding-bottom: 0.42857143em;
-}
-
-.ui.horizontal.relaxed.list .list > .item:not(:first-child),
-.ui.horizontal.relaxed.list > .item:not(:first-child) {
-  padding-left: 1rem;
-}
-
-.ui.horizontal.relaxed.list .list > .item:not(:last-child),
-.ui.horizontal.relaxed.list > .item:not(:last-child) {
-  padding-right: 1rem;
-}
-
-/* Very Relaxed */
-
-.ui[class*="very relaxed"].list:not(.horizontal) > .item:not(:first-child) {
-  padding-top: 0.85714286em;
-}
-
-.ui[class*="very relaxed"].list:not(.horizontal) > .item:not(:last-child) {
-  padding-bottom: 0.85714286em;
-}
-
-.ui.horizontal[class*="very relaxed"].list .list > .item:not(:first-child),
-.ui.horizontal[class*="very relaxed"].list > .item:not(:first-child) {
-  padding-left: 1.5rem;
-}
-
-.ui.horizontal[class*="very relaxed"].list .list > .item:not(:last-child),
-.ui.horizontal[class*="very relaxed"].list > .item:not(:last-child) {
-  padding-right: 1.5rem;
-}
-
-/*-------------------
-      Sizes
---------------------*/
-
-.ui.list {
-  font-size: 1em;
-}
-
-.ui.mini.list {
-  font-size: 0.78571429em;
-}
-
-.ui.mini.horizontal.list .list > .item,
-.ui.mini.horizontal.list > .item {
-  font-size: 0.78571429rem;
-}
-
-.ui.tiny.list {
-  font-size: 0.85714286em;
-}
-
-.ui.tiny.horizontal.list .list > .item,
-.ui.tiny.horizontal.list > .item {
-  font-size: 0.85714286rem;
-}
-
-.ui.small.list {
-  font-size: 0.92857143em;
-}
-
-.ui.small.horizontal.list .list > .item,
-.ui.small.horizontal.list > .item {
-  font-size: 0.92857143rem;
-}
-
-.ui.large.list {
-  font-size: 1.14285714em;
-}
-
-.ui.large.horizontal.list .list > .item,
-.ui.large.horizontal.list > .item {
-  font-size: 1.14285714rem;
-}
-
-.ui.big.list {
-  font-size: 1.28571429em;
-}
-
-.ui.big.horizontal.list .list > .item,
-.ui.big.horizontal.list > .item {
-  font-size: 1.28571429rem;
-}
-
-.ui.huge.list {
-  font-size: 1.42857143em;
-}
-
-.ui.huge.horizontal.list .list > .item,
-.ui.huge.horizontal.list > .item {
-  font-size: 1.42857143rem;
-}
-
-.ui.massive.list {
-  font-size: 1.71428571em;
-}
-
-.ui.massive.horizontal.list .list > .item,
-.ui.massive.horizontal.list > .item {
-  font-size: 1.71428571rem;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
-/*******************************
-    User Variable Overrides
-*******************************/
 /*
  * # Fomantic - Menu
  * http://github.com/fomantic/Fomantic-UI/
@@ -13306,7 +6764,6 @@ ol.ui.suffixed.list li:before,
   left: 100%;
   /* IE needs 0, all others support max-content to show dropdown icon inline, so keep both settings! */
   min-width: 0;
-  min-width: -webkit-max-content;
   min-width: -moz-max-content;
   min-width: max-content;
   margin: 0 0 0 0;
@@ -15030,690 +8487,6 @@ Floated Menu / Item
 /*******************************
          Site Overrides
 *******************************/
-/*!
- * # Fomantic-UI - Message
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            Message
-*******************************/
-
-.ui.message {
-  position: relative;
-  min-height: 1em;
-  margin: 1em 0;
-  background: #F8F8F9;
-  padding: 1em 1.5em;
-  line-height: 1.4285em;
-  color: rgba(0, 0, 0, 0.87);
-  transition: opacity 0.1s ease, color 0.1s ease, background 0.1s ease, box-shadow 0.1s ease;
-  border-radius: 0.28571429rem;
-  box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.22) inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.message:first-child {
-  margin-top: 0;
-}
-
-.ui.message:last-child {
-  margin-bottom: 0;
-}
-
-/*--------------
-     Content
----------------*/
-
-/* Header */
-
-.ui.message .header {
-  display: block;
-  font-family: var(--fonts-regular);
-  font-weight: 500;
-  margin: -0.14285714em 0 0 0;
-}
-
-/* Default font size */
-
-.ui.message .header:not(.ui) {
-  font-size: 1.14285714em;
-}
-
-/* Paragraph */
-
-.ui.message p {
-  opacity: 0.85;
-  margin: 0.75em 0;
-}
-
-.ui.message p:first-child {
-  margin-top: 0;
-}
-
-.ui.message p:last-child {
-  margin-bottom: 0;
-}
-
-.ui.message .header + p {
-  margin-top: 0.25em;
-}
-
-/* List */
-
-.ui.message .list:not(.ui) {
-  text-align: left;
-  padding: 0;
-  opacity: 0.85;
-  list-style-position: inside;
-  margin: 0.5em 0 0;
-}
-
-.ui.message .list:not(.ui):first-child {
-  margin-top: 0;
-}
-
-.ui.message .list:not(.ui):last-child {
-  margin-bottom: 0;
-}
-
-.ui.message .list:not(.ui) li {
-  position: relative;
-  list-style-type: none;
-  margin: 0 0 0.3em 1em;
-  padding: 0;
-}
-
-.ui.message .list:not(.ui) li:before {
-  position: absolute;
-  content: '•';
-  left: -1em;
-  height: 100%;
-  vertical-align: baseline;
-}
-
-.ui.message .list:not(.ui) li:last-child {
-  margin-bottom: 0;
-}
-
-/* Icon */
-
-.ui.message > i.icon {
-  margin-right: 0.6em;
-}
-
-/* Close Icon */
-
-.ui.message > .close.icon {
-  cursor: pointer;
-  position: absolute;
-  margin: 0;
-  top: 0.78575em;
-  right: 0.5em;
-  opacity: 0.7;
-  transition: opacity 0.1s ease;
-}
-
-.ui.message > .close.icon:hover {
-  opacity: 1;
-}
-
-/* First / Last Element */
-
-.ui.message > :first-child {
-  margin-top: 0;
-}
-
-.ui.message > :last-child {
-  margin-bottom: 0;
-}
-
-/*******************************
-            Coupling
-*******************************/
-
-.ui.dropdown .menu > .message {
-  margin: 0 -1px;
-}
-
-/*******************************
-            States
-*******************************/
-
-/*--------------
-    Visible
----------------*/
-
-.ui.visible.visible.visible.visible.message {
-  display: block;
-}
-
-.ui.icon.visible.visible.visible.visible.message {
-  display: flex;
-}
-
-/*--------------
-     Hidden
----------------*/
-
-.ui.hidden.hidden.hidden.hidden.message {
-  display: none;
-}
-
-/*******************************
-            Variations
-*******************************/
-
-/*--------------
-      Compact
-  ---------------*/
-
-.ui.compact.message {
-  display: inline-block;
-}
-
-.ui.compact.icon.message {
-  display: inline-flex;
-  width: auto;
-}
-
-/*--------------
-      Attached
-  ---------------*/
-
-.ui.attached.message {
-  margin-bottom: -1px;
-  border-radius: 0.28571429rem 0.28571429rem 0 0;
-  box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
-  margin-left: -1px;
-  margin-right: -1px;
-}
-
-.ui.attached + .ui.attached.message:not(.top):not(.bottom) {
-  margin-top: -1px;
-  border-radius: 0;
-}
-
-.ui.bottom.attached.message {
-  margin-top: -1px;
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
-  box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.15) inset, 0 1px 2px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.bottom.attached.message:not(:last-child) {
-  margin-bottom: 1em;
-}
-
-.ui.attached.icon.message {
-  width: auto;
-}
-
-/*--------------
-        Icon
-  ---------------*/
-
-.ui.icon.message {
-  display: flex;
-  width: 100%;
-  align-items: center;
-}
-
-.ui.icon.message > i.icon:not(.close) {
-  display: block;
-  flex: 0 0 auto;
-  width: auto;
-  line-height: 1;
-  vertical-align: middle;
-  font-size: 3em;
-  opacity: 0.8;
-}
-
-.ui.icon.message > .content {
-  display: block;
-  flex: 1 1 auto;
-  vertical-align: middle;
-}
-
-.ui.icon.message > i.icon:not(.close) + .content {
-  padding-left: 0;
-}
-
-.ui.icon.message > i.circular.icon {
-  width: 1em;
-}
-
-/*--------------
-      Floating
-  ---------------*/
-
-.ui.floating.message {
-  box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.22) inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-/*--------------
-     Colors
----------------*/
-
-/*--------------
-     Types
----------------*/
-
-.ui.positive.message {
-  background-color: #FCFFF5;
-  color: #2C662D;
-}
-
-.ui.positive.message,
-.ui.attached.positive.message {
-  box-shadow: 0 0 0 1px #A3C293 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.positive.message {
-  box-shadow: 0 0 0 1px #A3C293 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.positive.message .header {
-  color: #1A531B;
-}
-
-.ui.negative.message {
-  background-color: #FFF6F6;
-  color: #9F3A38;
-}
-
-.ui.negative.message,
-.ui.attached.negative.message {
-  box-shadow: 0 0 0 1px #E0B4B4 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.negative.message {
-  box-shadow: 0 0 0 1px #E0B4B4 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.negative.message .header {
-  color: #912D2B;
-}
-
-.ui.info.message {
-  background-color: #F8FFFF;
-  color: #276F86;
-}
-
-.ui.info.message,
-.ui.attached.info.message {
-  box-shadow: 0 0 0 1px #A9D5DE inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.info.message {
-  box-shadow: 0 0 0 1px #A9D5DE inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.info.message .header {
-  color: #0E566C;
-}
-
-.ui.warning.message {
-  background-color: #FFFAF3;
-  color: #573A08;
-}
-
-.ui.warning.message,
-.ui.attached.warning.message {
-  box-shadow: 0 0 0 1px #C9BA9B inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.warning.message {
-  box-shadow: 0 0 0 1px #C9BA9B inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.warning.message .header {
-  color: #794B02;
-}
-
-.ui.error.message {
-  background-color: #FFF6F6;
-  color: #9F3A38;
-}
-
-.ui.error.message,
-.ui.attached.error.message {
-  box-shadow: 0 0 0 1px #E0B4B4 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.error.message {
-  box-shadow: 0 0 0 1px #E0B4B4 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.error.message .header {
-  color: #912D2B;
-}
-
-.ui.success.message {
-  background-color: #FCFFF5;
-  color: #2C662D;
-}
-
-.ui.success.message,
-.ui.attached.success.message {
-  box-shadow: 0 0 0 1px #A3C293 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.success.message {
-  box-shadow: 0 0 0 1px #A3C293 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.success.message .header {
-  color: #1A531B;
-}
-
-.ui.primary.message {
-  background-color: #DFF0FF;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.primary.message,
-.ui.attached.primary.message {
-  box-shadow: 0 0 0 1px #2185D0 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.primary.message {
-  box-shadow: 0 0 0 1px #2185D0 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.primary.message .header {
-  color: rgba(242, 242, 242, 0.9);
-}
-
-.ui.secondary.message {
-  background-color: #F4F4F4;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.secondary.message,
-.ui.attached.secondary.message {
-  box-shadow: 0 0 0 1px #1B1C1D inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.secondary.message {
-  box-shadow: 0 0 0 1px #1B1C1D inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.secondary.message .header {
-  color: rgba(242, 242, 242, 0.9);
-}
-
-.ui.red.message {
-  background-color: #FFE8E6;
-  color: #DB2828;
-}
-
-.ui.red.message,
-.ui.attached.red.message {
-  box-shadow: 0 0 0 1px #DB2828 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.red.message {
-  box-shadow: 0 0 0 1px #DB2828 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.red.message .header {
-  color: #c82121;
-}
-
-.ui.orange.message {
-  background-color: #FFEDDE;
-  color: #F2711C;
-}
-
-.ui.orange.message,
-.ui.attached.orange.message {
-  box-shadow: 0 0 0 1px #F2711C inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.orange.message {
-  box-shadow: 0 0 0 1px #F2711C inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.orange.message .header {
-  color: #e7640d;
-}
-
-.ui.yellow.message {
-  background-color: #FFF8DB;
-  color: #B58105;
-}
-
-.ui.yellow.message,
-.ui.attached.yellow.message {
-  box-shadow: 0 0 0 1px #B58105 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.yellow.message {
-  box-shadow: 0 0 0 1px #B58105 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.yellow.message .header {
-  color: #9c6f04;
-}
-
-.ui.olive.message {
-  background-color: #FBFDEF;
-  color: #8ABC1E;
-}
-
-.ui.olive.message,
-.ui.attached.olive.message {
-  box-shadow: 0 0 0 1px #8ABC1E inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.olive.message {
-  box-shadow: 0 0 0 1px #8ABC1E inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.olive.message .header {
-  color: #7aa61a;
-}
-
-.ui.green.message {
-  background-color: #E5F9E7;
-  color: #1EBC30;
-}
-
-.ui.green.message,
-.ui.attached.green.message {
-  box-shadow: 0 0 0 1px #1EBC30 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.green.message {
-  box-shadow: 0 0 0 1px #1EBC30 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.green.message .header {
-  color: #1aa62a;
-}
-
-.ui.teal.message {
-  background-color: #E1F7F7;
-  color: #10A3A3;
-}
-
-.ui.teal.message,
-.ui.attached.teal.message {
-  box-shadow: 0 0 0 1px #10A3A3 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.teal.message {
-  box-shadow: 0 0 0 1px #10A3A3 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.teal.message .header {
-  color: #0e8c8c;
-}
-
-.ui.blue.message {
-  background-color: #DFF0FF;
-  color: #2185D0;
-}
-
-.ui.blue.message,
-.ui.attached.blue.message {
-  box-shadow: 0 0 0 1px #2185D0 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.blue.message {
-  box-shadow: 0 0 0 1px #2185D0 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.blue.message .header {
-  color: #1e77ba;
-}
-
-.ui.violet.message {
-  background-color: #EAE7FF;
-  color: #6435C9;
-}
-
-.ui.violet.message,
-.ui.attached.violet.message {
-  box-shadow: 0 0 0 1px #6435C9 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.violet.message {
-  box-shadow: 0 0 0 1px #6435C9 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.violet.message .header {
-  color: #5a30b5;
-}
-
-.ui.purple.message {
-  background-color: #F6E7FF;
-  color: #A333C8;
-}
-
-.ui.purple.message,
-.ui.attached.purple.message {
-  box-shadow: 0 0 0 1px #A333C8 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.purple.message {
-  box-shadow: 0 0 0 1px #A333C8 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.purple.message .header {
-  color: #922eb4;
-}
-
-.ui.pink.message {
-  background-color: #FFE3FB;
-  color: #E03997;
-}
-
-.ui.pink.message,
-.ui.attached.pink.message {
-  box-shadow: 0 0 0 1px #E03997 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.pink.message {
-  box-shadow: 0 0 0 1px #E03997 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.pink.message .header {
-  color: #dd238b;
-}
-
-.ui.brown.message {
-  background-color: #F1E2D3;
-  color: #A5673F;
-}
-
-.ui.brown.message,
-.ui.attached.brown.message {
-  box-shadow: 0 0 0 1px #A5673F inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.brown.message {
-  box-shadow: 0 0 0 1px #A5673F inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.brown.message .header {
-  color: #935b38;
-}
-
-.ui.grey.message {
-  background-color: #F4F4F4;
-  color: #767676;
-}
-
-.ui.grey.message,
-.ui.attached.grey.message {
-  box-shadow: 0 0 0 1px #767676 inset, 0 0 0 0 rgba(0, 0, 0, 0);
-}
-
-.ui.floating.grey.message {
-  box-shadow: 0 0 0 1px #767676 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui.grey.message .header {
-  color: #696969;
-}
-
-.ui.black.message {
-  background-color: #1B1C1D;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.black.message .header {
-  color: rgba(255, 255, 255, 0.9);
-}
-
-/*--------------
-     Sizes
----------------*/
-
-.ui.message {
-  font-size: 1em;
-}
-
-.ui.mini.message {
-  font-size: 0.78571429em;
-}
-
-.ui.tiny.message {
-  font-size: 0.85714286em;
-}
-
-.ui.small.message {
-  font-size: 0.92857143em;
-}
-
-.ui.large.message {
-  font-size: 1.14285714em;
-}
-
-.ui.big.message {
-  font-size: 1.28571429em;
-}
-
-.ui.huge.message {
-  font-size: 1.42857143em;
-}
-
-.ui.massive.message {
-  font-size: 1.71428571em;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
-/*******************************
-        Site Overrides
-*******************************/
 /*!
  * # Fomantic-UI - Modal
  * http://github.com/fomantic/Fomantic-UI/
@@ -16722,7 +9495,6 @@ Floated Menu / Item
   margin: -0.64285714em 0 0 -0.64285714em;
   width: 1.28571429em;
   height: 1.28571429em;
-  -webkit-animation: loader 0.6s infinite linear;
   animation: loader 0.6s infinite linear;
   border: 0.2em solid #767676;
   border-radius: 500rem;
@@ -16904,7 +9676,6 @@ Floated Menu / Item
 .ui.search.short > .results {
   overflow-x: hidden;
   overflow-y: auto;
-  -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
   -webkit-overflow-scrolling: touch;
 }
@@ -17077,853 +9848,6 @@ Floated Menu / Item
          Theme Overrides
 *******************************/
 
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Segment
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-            Segment
-*******************************/
-
-.ui.segment {
-  position: relative;
-  background: #FFFFFF;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15);
-  margin: 1rem 0;
-  padding: 1em 1em;
-  border-radius: 0.28571429rem;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.segment:first-child {
-  margin-top: 0;
-}
-
-.ui.segment:last-child {
-  margin-bottom: 0;
-}
-
-/* Vertical */
-
-.ui.vertical.segment {
-  margin: 0;
-  padding-left: 0;
-  padding-right: 0;
-  background: none transparent;
-  border-radius: 0;
-  box-shadow: none;
-  border: none;
-  border-bottom: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.vertical.segment:last-child {
-  border-bottom: none;
-}
-
-/*-------------------
-    Loose Coupling
---------------------*/
-
-/* Label */
-
-.ui[class*="bottom attached"].segment > [class*="top attached"].label {
-  border-top-left-radius: 0;
-  border-top-right-radius: 0;
-}
-
-.ui[class*="top attached"].segment > [class*="bottom attached"].label {
-  border-bottom-left-radius: 0;
-  border-bottom-right-radius: 0;
-}
-
-.ui.attached.segment:not(.top):not(.bottom) > [class*="top attached"].label {
-  border-top-left-radius: 0;
-  border-top-right-radius: 0;
-}
-
-.ui.attached.segment:not(.top):not(.bottom) > [class*="bottom attached"].label {
-  border-bottom-left-radius: 0;
-  border-bottom-right-radius: 0;
-}
-
-/* Grid */
-
-.ui.page.grid.segment,
-.ui.grid > .row > .ui.segment.column,
-.ui.grid > .ui.segment.column {
-  padding-top: 2em;
-  padding-bottom: 2em;
-}
-
-.ui.grid.segment {
-  margin: 1rem 0;
-  border-radius: 0.28571429rem;
-}
-
-/* Table */
-
-.ui.basic.table.segment {
-  background: #FFFFFF;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15);
-}
-
-.ui[class*="very basic"].table.segment {
-  padding: 1em 1em;
-}
-
-/* Tab */
-
-.ui.segment.tab:last-child {
-  margin-bottom: 1rem;
-}
-
-/*******************************
-             Types
-*******************************/
-
-/*-------------------
-       Placeholder
-  --------------------*/
-
-.ui.placeholder.segment {
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: stretch;
-  max-width: initial;
-  -webkit-animation: none;
-  animation: none;
-  overflow: visible;
-  padding: 1em 1em;
-  min-height: 18rem;
-  background: #F9FAFB;
-  border-color: rgba(34, 36, 38, 0.15);
-  box-shadow: 0 2px 25px 0 rgba(34, 36, 38, 0.05) inset;
-}
-
-.ui.placeholder.segment .button,
-.ui.placeholder.segment textarea {
-  display: block;
-}
-
-.ui.placeholder.segment .field,
-.ui.placeholder.segment textarea,
-.ui.placeholder.segment > .ui.input,
-.ui.placeholder.segment .button {
-  max-width: 15rem;
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.ui.placeholder.segment .column .button,
-.ui.placeholder.segment .column .field,
-.ui.placeholder.segment .column textarea,
-.ui.placeholder.segment .column > .ui.input {
-  max-width: 15rem;
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.ui.placeholder.segment > .inline {
-  align-self: center;
-}
-
-.ui.placeholder.segment > .inline > .button {
-  display: inline-block;
-  width: auto;
-  margin: 0 0.35714286rem 0 0;
-}
-
-.ui.placeholder.segment > .inline > .button:last-child {
-  margin-right: 0;
-}
-
-/*-------------------
-         Padded
-  --------------------*/
-
-.ui.padded.segment {
-  padding: 1.5em;
-}
-
-.ui[class*="very padded"].segment {
-  padding: 3em;
-}
-
-/* Padded vertical */
-
-.ui.padded.segment.vertical.segment,
-.ui[class*="very padded"].vertical.segment {
-  padding-left: 0;
-  padding-right: 0;
-}
-
-/*-------------------
-         Compact
-  --------------------*/
-
-.ui.compact.segment {
-  display: table;
-}
-
-/* Compact Group */
-
-.ui.compact.segments {
-  display: inline-flex;
-}
-
-.ui.compact.segments .segment,
-.ui.segments .compact.segment {
-  display: block;
-  flex: 0 1 auto;
-}
-
-/*-------------------
-         Circular
-  --------------------*/
-
-.ui.circular.segment {
-  display: table-cell;
-  padding: 2em;
-  text-align: center;
-  vertical-align: middle;
-  border-radius: 500em;
-}
-
-/*-------------------
-         Raised
-  --------------------*/
-
-.ui.raised.raised.segments,
-.ui.raised.raised.segment {
-  box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
-}
-
-/*******************************
-              Groups
-  *******************************/
-
-/* Group */
-
-.ui.segments {
-  flex-direction: column;
-  position: relative;
-  margin: 1rem 0;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15);
-  border-radius: 0.28571429rem;
-}
-
-.ui.segments:first-child {
-  margin-top: 0;
-}
-
-.ui.segments:last-child {
-  margin-bottom: 0;
-}
-
-/* Nested Segment */
-
-.ui.segments > .segment {
-  top: 0;
-  bottom: 0;
-  border-radius: 0;
-  margin: 0;
-  width: auto;
-  box-shadow: none;
-  border: none;
-  border-top: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.segments:not(.horizontal) > .segment:first-child {
-  top: 0;
-  bottom: 0;
-  border-top: none;
-  margin-top: 0;
-  margin-bottom: 0;
-  border-radius: 0.28571429rem 0.28571429rem 0 0;
-}
-
-/* Bottom */
-
-.ui.segments:not(.horizontal) > .segment:last-child {
-  top: 0;
-  bottom: 0;
-  margin-top: 0;
-  margin-bottom: 0;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), none;
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
-}
-
-/* Only */
-
-.ui.segments:not(.horizontal) > .segment:only-child {
-  border-radius: 0.28571429rem;
-}
-
-/* Nested Group */
-
-.ui.segments > .ui.segments {
-  border-top: 1px solid rgba(34, 36, 38, 0.15);
-  margin: 1rem 1rem;
-}
-
-.ui.segments > .segments:first-child {
-  border-top: none;
-}
-
-.ui.segments > .segment + .segments:not(.horizontal) {
-  margin-top: 0;
-}
-
-/* Horizontal Group */
-
-.ui.horizontal.segments {
-  display: flex;
-  flex-direction: row;
-  background-color: transparent;
-  padding: 0;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15);
-  margin: 1rem 0;
-  border-radius: 0.28571429rem;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.stackable.horizontal.segments {
-  flex-wrap: wrap;
-}
-
-/* Nested Horizontal Group */
-
-.ui.segments > .horizontal.segments {
-  margin: 0;
-  background-color: transparent;
-  border-radius: 0;
-  border: none;
-  box-shadow: none;
-  border-top: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-/* Horizontal Segment */
-
-.ui.horizontal.segments:not(.compact) > .segment:not(.compact) {
-  flex: 1 1 auto;
-  -ms-flex: 1 1 0;
-  /* Solves #2550 MS Flex */
-}
-
-.ui.horizontal.segments > .segment {
-  margin: 0;
-  min-width: 0;
-  border-radius: 0;
-  border: none;
-  box-shadow: none;
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-/* Border Fixes */
-
-.ui.segments > .horizontal.segments:first-child {
-  border-top: none;
-}
-
-.ui.horizontal.segments:not(.stackable) > .segment:first-child {
-  border-left: none;
-}
-
-.ui.horizontal.segments > .segment:first-child {
-  border-radius: 0.28571429rem 0 0 0.28571429rem;
-}
-
-.ui.horizontal.segments > .segment:last-child {
-  border-radius: 0 0.28571429rem 0.28571429rem 0;
-}
-
-/*******************************
-            States
-*******************************/
-
-/*--------------
-      Disabled
-  ---------------*/
-
-.ui.disabled.segment {
-  opacity: var(--opacity-disabled);
-  color: rgba(40, 40, 40, 0.3);
-}
-
-/*--------------
-      Loading
-  ---------------*/
-
-.ui.loading.segment {
-  position: relative;
-  cursor: default;
-  pointer-events: none;
-  text-shadow: none !important;
-  transition: all 0s linear;
-}
-
-.ui.loading.segment:before {
-  position: absolute;
-  content: '';
-  top: 0;
-  left: 0;
-  background: rgba(255, 255, 255, 0.8);
-  width: 100%;
-  height: 100%;
-  border-radius: 0.28571429rem;
-  z-index: 100;
-}
-
-.ui.loading.segment:after {
-  position: absolute;
-  content: '';
-  top: 50%;
-  left: 50%;
-  margin: -1.5em 0 0 -1.5em;
-  width: 3em;
-  height: 3em;
-  -webkit-animation: loader 0.6s infinite linear;
-  animation: loader 0.6s infinite linear;
-  border: 0.2em solid #767676;
-  border-radius: 500rem;
-  box-shadow: 0 0 0 1px transparent;
-  visibility: visible;
-  z-index: 101;
-}
-
-/*******************************
-           Variations
-*******************************/
-
-/*-------------------
-         Basic
-  --------------------*/
-
-.ui.basic.segment,
-.ui.segments .ui.basic.segment,
-.ui.basic.segments {
-  background: none transparent;
-  box-shadow: none;
-  border: none;
-  border-radius: 0;
-}
-
-/*-------------------
-         Clearing
-  --------------------*/
-
-.ui.clearing.segment:after {
-  content: "";
-  display: block;
-  clear: both;
-}
-
-/*-------------------
-       Colors
---------------------*/
-
-.ui.red.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #DB2828;
-}
-
-.ui.orange.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #F2711C;
-}
-
-.ui.yellow.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #FBBD08;
-}
-
-.ui.olive.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #B5CC18;
-}
-
-.ui.green.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #21BA45;
-}
-
-.ui.teal.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #00B5AD;
-}
-
-.ui.blue.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #2185D0;
-}
-
-.ui.violet.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #6435C9;
-}
-
-.ui.purple.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #A333C8;
-}
-
-.ui.pink.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #E03997;
-}
-
-.ui.brown.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #A5673F;
-}
-
-.ui.grey.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #767676;
-}
-
-.ui.black.segment.segment.segment.segment.segment:not(.inverted) {
-  border-top: 2px solid #1B1C1D;
-}
-
-/*-------------------
-         Aligned
-  --------------------*/
-
-.ui[class*="left aligned"].segment {
-  text-align: left;
-}
-
-.ui[class*="right aligned"].segment {
-  text-align: right;
-}
-
-.ui[class*="center aligned"].segment {
-  text-align: center;
-}
-
-/*-------------------
-         Floated
-  --------------------*/
-
-.ui.floated.segment,
-.ui[class*="left floated"].segment {
-  float: left;
-  margin-right: 1em;
-}
-
-.ui[class*="right floated"].segment {
-  float: right;
-  margin-left: 1em;
-}
-
-/*-------------------
-     Emphasis
---------------------*/
-
-/* Secondary */
-
-.ui.secondary.segment {
-  background: #F3F4F5;
-  color: rgba(0, 0, 0, 0.6);
-}
-
-/* Tertiary */
-
-.ui.tertiary.segment {
-  background: #DCDDDE;
-  color: rgba(0, 0, 0, 0.6);
-}
-
-/*-------------------
-        Attached
-  --------------------*/
-
-/* Middle */
-
-.ui.attached.segment {
-  top: 0;
-  bottom: 0;
-  border-radius: 0;
-  margin: 0 -1px;
-  width: calc(100% + 2px);
-  max-width: calc(100% + 2px);
-  box-shadow: none;
-  border: 1px solid #D4D4D5;
-}
-
-.ui.attached:not(.message) + .ui.attached.segment:not(.top) {
-  border-top: none;
-}
-
-/* Top */
-
-.ui[class*="top attached"].segment {
-  bottom: 0;
-  margin-bottom: 0;
-  top: 0;
-  margin-top: 1rem;
-  border-radius: 0.28571429rem 0.28571429rem 0 0;
-}
-
-.ui.segment[class*="top attached"]:first-child {
-  margin-top: 0;
-}
-
-/* Bottom */
-
-.ui.segment[class*="bottom attached"] {
-  bottom: 0;
-  margin-top: 0;
-  top: 0;
-  margin-bottom: 1rem;
-  box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), none;
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
-}
-
-.ui.segment[class*="bottom attached"]:last-child {
-  margin-bottom: 1rem;
-}
-
-/*--------------
-       Fitted
-  ---------------*/
-
-.ui.fitted.segment:not(.horizontally) {
-  padding-top: 0;
-  padding-bottom: 0;
-}
-
-.ui.fitted.segment:not(.vertically) {
-  padding-left: 0;
-  padding-right: 0;
-}
-
-/*-------------------
-        Size
---------------------*/
-
-.ui.segments .segment,
-.ui.segment {
-  font-size: 1rem;
-}
-
-.ui.mini.segments .segment,
-.ui.mini.segment {
-  font-size: 0.78571429rem;
-}
-
-.ui.tiny.segments .segment,
-.ui.tiny.segment {
-  font-size: 0.85714286rem;
-}
-
-.ui.small.segments .segment,
-.ui.small.segment {
-  font-size: 0.92857143rem;
-}
-
-.ui.large.segments .segment,
-.ui.large.segment {
-  font-size: 1.14285714rem;
-}
-
-.ui.big.segments .segment,
-.ui.big.segment {
-  font-size: 1.28571429rem;
-}
-
-.ui.huge.segments .segment,
-.ui.huge.segment {
-  font-size: 1.42857143rem;
-}
-
-.ui.massive.segments .segment,
-.ui.massive.segment {
-  font-size: 1.71428571rem;
-}
-
-/*******************************
-         Theme Overrides
-*******************************/
-
-/*******************************
-         Site Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Site
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-             Page
-*******************************/
-
-html,
-body {
-  height: 100%;
-}
-
-html {
-  font-size: 14px;
-}
-
-body {
-  margin: 0;
-  padding: 0;
-  overflow-x: visible;
-  min-width: 320px;
-  background: #FFFFFF;
-  font-family: var(--fonts-regular);
-  font-size: 14px;
-  line-height: 1.4285em;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-/*******************************
-             Headers
-*******************************/
-
-h1,
-h2,
-h3,
-h4,
-h5 {
-  font-family: var(--fonts-regular);
-  line-height: 1.28571429em;
-  margin: calc(2rem - 0.1428571428571429em) 0 1rem;
-  font-weight: 500;
-  padding: 0;
-}
-
-h1 {
-  min-height: 1rem;
-  font-size: 2rem;
-}
-
-h2 {
-  font-size: 1.71428571rem;
-}
-
-h3 {
-  font-size: 1.28571429rem;
-}
-
-h4 {
-  font-size: 1.07142857rem;
-}
-
-h5 {
-  font-size: 1rem;
-}
-
-h1:first-child,
-h2:first-child,
-h3:first-child,
-h4:first-child,
-h5:first-child {
-  margin-top: 0;
-}
-
-h1:last-child,
-h2:last-child,
-h3:last-child,
-h4:last-child,
-h5:last-child {
-  margin-bottom: 0;
-}
-
-/*******************************
-             Text
-*******************************/
-
-p {
-  margin: 0 0 1em;
-  line-height: 1.4285em;
-}
-
-p:first-child {
-  margin-top: 0;
-}
-
-p:last-child {
-  margin-bottom: 0;
-}
-
-/*-------------------
-        Links
---------------------*/
-
-a {
-  color: #4183C4;
-  text-decoration: none;
-}
-
-a:hover {
-  color: #1e70bf;
-  text-decoration: underline;
-}
-
-/*******************************
-         Scrollbars
-*******************************/
-
-/*******************************
-          Highlighting
-*******************************/
-
-/* Site */
-
-::-webkit-selection {
-  background-color: #CCE2FF;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-::-moz-selection {
-  background-color: #CCE2FF;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-::selection {
-  background-color: #CCE2FF;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-/* Form */
-
-textarea::-webkit-selection,
-input::-webkit-selection {
-  background-color: rgba(100, 100, 100, 0.4);
-  color: rgba(0, 0, 0, 0.87);
-}
-
-textarea::-moz-selection,
-input::-moz-selection {
-  background-color: rgba(100, 100, 100, 0.4);
-  color: rgba(0, 0, 0, 0.87);
-}
-
-textarea::-moz-selection,
-input::-moz-selection {
-  background-color: rgba(100, 100, 100, 0.4);
-  color: rgba(0, 0, 0, 0.87);
-}
-
-textarea::selection,
-input::selection {
-  background-color: rgba(100, 100, 100, 0.4);
-  color: rgba(0, 0, 0, 0.87);
-}
-
-/*******************************
-        Global Overrides
-*******************************/
-
 /*******************************
          Site Overrides
 *******************************/
@@ -17996,7 +9920,6 @@ input::selection {
   margin: -1.25em 0 0 -1.25em;
   width: 2.5em;
   height: 2.5em;
-  -webkit-animation: loader 0.6s infinite linear;
   animation: loader 0.6s infinite linear;
   border: 0.2em solid #767676;
   border-radius: 500rem;
@@ -18009,1366 +9932,4 @@ input::selection {
 
 /*******************************
         User Overrides
-*******************************/
-/*!
- * # Fomantic-UI - Table
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-/*******************************
-             Table
-*******************************/
-
-/* Prototype */
-
-.ui.table {
-  width: 100%;
-  background: #FFFFFF;
-  margin: 1em 0;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-  box-shadow: none;
-  border-radius: 0.28571429rem;
-  text-align: left;
-  vertical-align: middle;
-  color: rgba(0, 0, 0, 0.87);
-  border-collapse: separate;
-  border-spacing: 0;
-}
-
-.ui.table:first-child {
-  margin-top: 0;
-}
-
-.ui.table:last-child {
-  margin-bottom: 0;
-}
-
-.ui.table > thead,
-.ui.table > tbody {
-  text-align: inherit;
-  vertical-align: inherit;
-}
-
-/*******************************
-             Parts
-*******************************/
-
-/* Table Content */
-
-.ui.table th,
-.ui.table td {
-  transition: background 0.1s ease, color 0.1s ease;
-}
-
-/* Rowspan helper class */
-
-.ui.table th.rowspanned,
-.ui.table td.rowspanned {
-  display: none;
-}
-
-/* Headers */
-
-.ui.table > thead {
-  box-shadow: none;
-}
-
-.ui.table > thead > tr > th {
-  cursor: auto;
-  background: #F9FAFB;
-  text-align: inherit;
-  color: rgba(0, 0, 0, 0.87);
-  padding: 0.92857143em 0.78571429em;
-  vertical-align: inherit;
-  font-style: none;
-  font-weight: 500;
-  text-transform: none;
-  border-bottom: 1px solid rgba(34, 36, 38, 0.1);
-  border-left: none;
-}
-
-.ui.table > thead > tr > th:first-child {
-  border-left: none;
-}
-
-.ui.table > thead > tr:first-child > th:first-child {
-  border-radius: 0.28571429rem 0 0 0;
-}
-
-.ui.table > thead > tr:first-child > th:last-child {
-  border-radius: 0 0.28571429rem 0 0;
-}
-
-.ui.table > thead > tr:first-child > th:only-child {
-  border-radius: 0.28571429rem 0.28571429rem 0 0;
-}
-
-/* Footer */
-
-.ui.table > tfoot {
-  box-shadow: none;
-}
-
-.ui.table > tfoot > tr > th,
-.ui.table > tfoot > tr > td {
-  cursor: auto;
-  border-top: 1px solid rgba(34, 36, 38, 0.15);
-  background: #F9FAFB;
-  text-align: inherit;
-  color: rgba(0, 0, 0, 0.87);
-  padding: 0.78571429em 0.78571429em;
-  vertical-align: inherit;
-  font-style: normal;
-  font-weight: normal;
-  text-transform: none;
-}
-
-.ui.table > tfoot > tr > th:first-child,
-.ui.table > tfoot > tr > td:first-child {
-  border-left: none;
-}
-
-.ui.table > tfoot > tr:first-child > th:first-child,
-.ui.table > tfoot > tr:first-child > td:first-child {
-  border-radius: 0 0 0 0.28571429rem;
-}
-
-.ui.table > tfoot > tr:first-child > th:last-child,
-.ui.table > tfoot > tr:first-child > td:last-child {
-  border-radius: 0 0 0.28571429rem 0;
-}
-
-.ui.table > tfoot > tr:first-child > th:only-child,
-.ui.table > tfoot > tr:first-child > td:only-child {
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
-}
-
-/* Table Row */
-
-.ui.table > tr > td,
-.ui.table > tbody > tr > td {
-  border-top: 1px solid rgba(34, 36, 38, 0.1);
-}
-
-.ui.table > tr:first-child > td,
-.ui.table > tbody > tr:first-child > td {
-  border-top: none;
-}
-
-/* Repeated tbody */
-
-.ui.table > tbody + tbody tr:first-child > td {
-  border-top: 1px solid rgba(34, 36, 38, 0.1);
-}
-
-/* Table Cells */
-
-.ui.table > tbody > tr > td,
-.ui.table > tr > td {
-  padding: 0.78571429em 0.78571429em;
-  text-align: inherit;
-}
-
-/* Icons */
-
-.ui.table > i.icon {
-  vertical-align: baseline;
-}
-
-.ui.table > i.icon:only-child {
-  margin: 0;
-}
-
-/* Table Segment */
-
-.ui.table.segment {
-  padding: 0;
-}
-
-.ui.table.segment:after {
-  display: none;
-}
-
-.ui.table.segment.stacked:after {
-  display: block;
-}
-
-/* Responsive */
-
-@media only screen and (max-width: 767.98px) {
-  .ui.table:not(.unstackable) {
-    width: 100%;
-    padding: 0;
-  }
-
-  .ui.table:not(.unstackable) > thead,
-  .ui.table:not(.unstackable) > thead > tr,
-  .ui.table:not(.unstackable) > tfoot,
-  .ui.table:not(.unstackable) > tfoot > tr,
-  .ui.table:not(.unstackable) > tbody,
-  .ui.table:not(.unstackable) > tr,
-  .ui.table:not(.unstackable) > tbody > tr,
-  .ui.table:not(.unstackable) > tr > th:not(.rowspanned),
-  .ui.table:not(.unstackable) > thead > tr > th:not(.rowspanned),
-  .ui.table:not(.unstackable) > tbody > tr > th:not(.rowspanned),
-  .ui.table:not(.unstackable) > tfoot > tr > th:not(.rowspanned),
-  .ui.table:not(.unstackable) > tr > td:not(.rowspanned),
-  .ui.table:not(.unstackable) > tbody > tr > td:not(.rowspanned),
-  .ui.table:not(.unstackable) > tfoot > tr > td:not(.rowspanned) {
-    display: block !important;
-    width: auto !important;
-  }
-
-  .ui.table:not(.unstackable) > thead {
-    display: block;
-  }
-
-  .ui.table:not(.unstackable) > tfoot {
-    display: block;
-  }
-
-  .ui.ui.ui.ui.table:not(.unstackable) > tr,
-  .ui.ui.ui.ui.table:not(.unstackable) > thead > tr,
-  .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr,
-  .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr {
-    padding-top: 1em;
-    padding-bottom: 1em;
-    box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.1) inset;
-  }
-
-  .ui.ui.ui.ui.table:not(.unstackable) > tr > th,
-  .ui.ui.ui.ui.table:not(.unstackable) > thead > tr > th,
-  .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > th,
-  .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > th,
-  .ui.ui.ui.ui.table:not(.unstackable) > tr > td,
-  .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > td,
-  .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > td {
-    background: none;
-    border: none;
-    padding: 0.25em 0.75em;
-    box-shadow: none;
-  }
-
-  .ui.table:not(.unstackable) > tr > th:first-child,
-  .ui.table:not(.unstackable) > thead > tr > th:first-child,
-  .ui.table:not(.unstackable) > tbody > tr > th:first-child,
-  .ui.table:not(.unstackable) > tfoot > tr > th:first-child,
-  .ui.table:not(.unstackable) > tr > td:first-child,
-  .ui.table:not(.unstackable) > tbody > tr > td:first-child,
-  .ui.table:not(.unstackable) > tfoot > tr > td:first-child {
-    font-weight: 500;
-  }
-
-  /* Definition Table */
-
-  .ui.definition.table:not(.unstackable) > thead > tr > th:first-child {
-    box-shadow: none !important;
-  }
-}
-
-/*******************************
-            Coupling
-*******************************/
-
-/* UI Image */
-
-.ui.table .collapsing .image,
-.ui.table .collapsing .image img {
-  max-width: none;
-}
-
-/*******************************
-             Types
-*******************************/
-
-/*--------------
-    Complex
----------------*/
-
-.ui.structured.table {
-  border-collapse: collapse;
-}
-
-.ui.structured.table > thead > tr > th {
-  border-left: none;
-  border-right: none;
-}
-
-.ui.structured.sortable.table > thead > tr > th {
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-  border-right: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.structured.basic.table > tr > th,
-.ui.structured.basic.table > thead > tr > th,
-.ui.structured.basic.table > tbody > tr > th,
-.ui.structured.basic.table > tfoot > tr > th {
-  border-left: none;
-  border-right: none;
-}
-
-.ui.structured.celled.table > tr > th,
-.ui.structured.celled.table > thead > tr > th,
-.ui.structured.celled.table > tbody > tr > th,
-.ui.structured.celled.table > tfoot > tr > th,
-.ui.structured.celled.table > tr > td,
-.ui.structured.celled.table > tbody > tr > td,
-.ui.structured.celled.table > tfoot > tr > td {
-  border-left: 1px solid rgba(34, 36, 38, 0.1);
-  border-right: 1px solid rgba(34, 36, 38, 0.1);
-}
-
-/*--------------
-     Definition
-  ---------------*/
-
-.ui.definition.table > thead:not(.full-width) > tr > th:first-child {
-  pointer-events: none;
-  background: #FFFFFF;
-  font-weight: normal;
-  color: rgba(0, 0, 0, 0.4);
-  box-shadow: -0.1em -0.2em 0 0.1em #FFFFFF;
-  -moz-transform: scale(1);
-}
-
-.ui.definition.table > tfoot:not(.full-width) > tr > th:first-child {
-  pointer-events: none;
-  background: #FFFFFF;
-  font-weight: normal;
-  color: rgba(0, 0, 0, 0.4);
-  box-shadow: -0.1em 0.2em 0 0.1em #FFFFFF;
-  -moz-transform: scale(1);
-}
-
-/* Highlight Defining Column */
-
-.ui.definition.table > tr > td:first-child:not(.ignored),
-.ui.definition.table > tbody > tr > td:first-child:not(.ignored),
-.ui.definition.table > tfoot > tr > td:first-child:not(.ignored),
-.ui.definition.table tr td.definition {
-  background: rgba(0, 0, 0, 0.03);
-  font-weight: 500;
-  color: rgba(0, 0, 0, 0.95);
-  text-transform: '';
-  box-shadow: '';
-  text-align: '';
-  font-size: 1em;
-  padding-left: '';
-  padding-right: '';
-}
-
-/* Fix 2nd Column */
-
-.ui.definition.table > thead:not(.full-width) > tr > th:nth-child(2) {
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.definition.table > tfoot:not(.full-width) > tr > th:nth-child(2),
-.ui.definition.table > tfoot:not(.full-width) > tr > td:nth-child(2) {
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-.ui.definition.table > tr > td:nth-child(2),
-.ui.definition.table > tbody > tr > td:nth-child(2) {
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-}
-
-/*******************************
-             States
-*******************************/
-
-/*--------------
-      Positive
-  ---------------*/
-
-.ui.ui.ui.ui.table tr.positive,
-.ui.ui.table td.positive {
-  box-shadow: 0 0 0 #A3C293 inset;
-  background: #FCFFF5;
-  color: #2C662D;
-}
-
-/*--------------
-       Negative
-  ---------------*/
-
-.ui.ui.ui.ui.table tr.negative,
-.ui.ui.table td.negative {
-  box-shadow: 0 0 0 #E0B4B4 inset;
-  background: #FFF6F6;
-  color: #9F3A38;
-}
-
-/*--------------
-        Error
-  ---------------*/
-
-.ui.ui.ui.ui.table tr.error,
-.ui.ui.table td.error {
-  box-shadow: 0 0 0 #E0B4B4 inset;
-  background: #FFF6F6;
-  color: #9F3A38;
-}
-
-/*--------------
-       Warning
-  ---------------*/
-
-.ui.ui.ui.ui.table tr.warning,
-.ui.ui.table td.warning {
-  box-shadow: 0 0 0 #C9BA9B inset;
-  background: #FFFAF3;
-  color: #573A08;
-}
-
-/*--------------
-       Active
-  ---------------*/
-
-.ui.ui.ui.ui.table tr.active,
-.ui.ui.table td.active {
-  box-shadow: 0 0 0 rgba(0, 0, 0, 0.87) inset;
-  background: #E0E0E0;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-/*--------------
-       Disabled
-  ---------------*/
-
-.ui.table tr.disabled td,
-.ui.table tr td.disabled,
-.ui.table tr.disabled:hover,
-.ui.table tr:hover td.disabled {
-  pointer-events: none;
-  color: rgba(40, 40, 40, 0.3);
-}
-
-/*******************************
-          Variations
-*******************************/
-
-/*--------------
-   Text Alignment
-  ---------------*/
-
-.ui.table[class*="left aligned"],
-.ui.table [class*="left aligned"] {
-  text-align: left;
-}
-
-.ui.table[class*="center aligned"],
-.ui.table [class*="center aligned"] {
-  text-align: center;
-}
-
-.ui.table[class*="right aligned"],
-.ui.table [class*="right aligned"] {
-  text-align: right;
-}
-
-/*------------------
-   Vertical Alignment
-  ------------------*/
-
-.ui.table[class*="top aligned"],
-.ui.table [class*="top aligned"] {
-  vertical-align: top;
-}
-
-.ui.table[class*="middle aligned"],
-.ui.table [class*="middle aligned"] {
-  vertical-align: middle;
-}
-
-.ui.table[class*="bottom aligned"],
-.ui.table [class*="bottom aligned"] {
-  vertical-align: bottom;
-}
-
-/*--------------
-      Collapsing
-  ---------------*/
-
-.ui.table th.collapsing,
-.ui.table td.collapsing {
-  width: 1px;
-  white-space: nowrap;
-}
-
-/*--------------
-       Fixed
-  ---------------*/
-
-.ui.fixed.table {
-  table-layout: fixed;
-}
-
-.ui.fixed.table th,
-.ui.fixed.table td {
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-/*--------------
-     Selectable
-  ---------------*/
-
-.ui.ui.selectable.table > tbody > tr:hover,
-.ui.table tbody tr td.selectable:hover {
-  background: rgba(0, 0, 0, 0.05);
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/* Selectable Cell Link */
-
-.ui.table tbody tr td.selectable {
-  padding: 0;
-}
-
-.ui.table tbody tr td.selectable > a:not(.ui) {
-  display: block;
-  color: inherit;
-  padding: 0.78571429em 0.78571429em;
-}
-
-.ui.table > tr > td.selectable,
-.ui.table > tbody > tr > td.selectable,
-.ui.selectable.table > tbody > tr,
-.ui.selectable.table > tr {
-  cursor: pointer;
-}
-
-/* Other States */
-
-.ui.ui.selectable.table tr.error:hover,
-.ui.table tr td.selectable.error:hover,
-.ui.selectable.table tr:hover td.error {
-  background: #ffe7e7;
-  color: #943634;
-}
-
-.ui.ui.selectable.table tr.warning:hover,
-.ui.table tr td.selectable.warning:hover,
-.ui.selectable.table tr:hover td.warning {
-  background: #fff4e4;
-  color: #493107;
-}
-
-.ui.ui.selectable.table tr.active:hover,
-.ui.table tr td.selectable.active:hover,
-.ui.selectable.table tr:hover td.active {
-  background: #E0E0E0;
-  color: rgba(0, 0, 0, 0.87);
-}
-
-.ui.ui.selectable.table tr.positive:hover,
-.ui.table tr td.selectable.positive:hover,
-.ui.selectable.table tr:hover td.positive {
-  background: #f7ffe6;
-  color: #275b28;
-}
-
-.ui.ui.selectable.table tr.negative:hover,
-.ui.table tr td.selectable.negative:hover,
-.ui.selectable.table tr:hover td.negative {
-  background: #ffe7e7;
-  color: #943634;
-}
-
-/*-------------------
-        Attached
-  --------------------*/
-
-/* Middle */
-
-.ui.attached.table {
-  top: 0;
-  bottom: 0;
-  border-radius: 0;
-  margin: 0 -1px;
-  width: calc(100% + 2px);
-  max-width: calc(100% + 2px);
-  box-shadow: none;
-  border: 1px solid #D4D4D5;
-}
-
-.ui.attached + .ui.attached.table:not(.top) {
-  border-top: none;
-}
-
-/* Top */
-
-.ui[class*="top attached"].table {
-  bottom: 0;
-  margin-bottom: 0;
-  top: 0;
-  margin-top: 1em;
-  border-radius: 0.28571429rem 0.28571429rem 0 0;
-}
-
-.ui.table[class*="top attached"]:first-child {
-  margin-top: 0;
-}
-
-/* Bottom */
-
-.ui[class*="bottom attached"].table {
-  bottom: 0;
-  margin-top: 0;
-  top: 0;
-  margin-bottom: 1em;
-  box-shadow: none, none;
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
-}
-
-.ui[class*="bottom attached"].table:last-child {
-  margin-bottom: 0;
-}
-
-/*--------------
-       Striped
-  ---------------*/
-
-/* Table Striping */
-
-.ui.striped.table > tr:nth-child(2n),
-.ui.striped.table > tbody > tr:nth-child(2n) {
-  background-color: rgba(0, 0, 50, 0.02);
-}
-
-/* Allow striped active hover */
-
-.ui.striped.selectable.selectable.selectable.table tbody tr.active:hover {
-  background: #EFEFEF;
-  color: rgba(0, 0, 0, 0.95);
-}
-
-/*--------------
-   Single Line
----------------*/
-
-.ui.table[class*="single line"],
-.ui.table [class*="single line"] {
-  white-space: nowrap;
-}
-
-/*-------------------
-       Colors
---------------------*/
-
-.ui.primary.table {
-  border-top: 0.2em solid #2185D0;
-}
-
-.ui.ui.ui.ui.table tr.primary:not(.marked),
-.ui.ui.table td.primary:not(.marked) {
-  background: #ddf4ff;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.ui.selectable.table tr.primary:not(.marked):hover,
-.ui.table tr td.selectable.primary:not(.marked):hover,
-.ui.selectable.table tr:hover td.primary:not(.marked) {
-  background: #d3f1ff;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.secondary.table {
-  border-top: 0.2em solid #1B1C1D;
-}
-
-.ui.ui.ui.ui.table tr.secondary:not(.marked),
-.ui.ui.table td.secondary:not(.marked) {
-  background: #dddddd;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.ui.selectable.table tr.secondary:not(.marked):hover,
-.ui.table tr td.selectable.secondary:not(.marked):hover,
-.ui.selectable.table tr:hover td.secondary:not(.marked) {
-  background: #e2e2e2;
-  color: rgba(255, 255, 255, 0.9);
-}
-
-.ui.red.table {
-  border-top: 0.2em solid #DB2828;
-}
-
-.ui.ui.ui.ui.table tr.red:not(.marked),
-.ui.ui.table td.red:not(.marked) {
-  background: #ffe1df;
-  color: #DB2828;
-}
-
-.ui.ui.selectable.table tr.red:not(.marked):hover,
-.ui.table tr td.selectable.red:not(.marked):hover,
-.ui.selectable.table tr:hover td.red:not(.marked) {
-  background: #ffd7d5;
-  color: #DB2828;
-}
-
-.ui.orange.table {
-  border-top: 0.2em solid #F2711C;
-}
-
-.ui.ui.ui.ui.table tr.orange:not(.marked),
-.ui.ui.table td.orange:not(.marked) {
-  background: #ffe7d1;
-  color: #F2711C;
-}
-
-.ui.ui.selectable.table tr.orange:not(.marked):hover,
-.ui.table tr td.selectable.orange:not(.marked):hover,
-.ui.selectable.table tr:hover td.orange:not(.marked) {
-  background: #fae1cc;
-  color: #F2711C;
-}
-
-.ui.yellow.table {
-  border-top: 0.2em solid #FBBD08;
-}
-
-.ui.ui.ui.ui.table tr.yellow:not(.marked),
-.ui.ui.table td.yellow:not(.marked) {
-  background: #fff9d2;
-  color: #B58105;
-}
-
-.ui.ui.selectable.table tr.yellow:not(.marked):hover,
-.ui.table tr td.selectable.yellow:not(.marked):hover,
-.ui.selectable.table tr:hover td.yellow:not(.marked) {
-  background: #fbf5cc;
-  color: #B58105;
-}
-
-.ui.olive.table {
-  border-top: 0.2em solid #B5CC18;
-}
-
-.ui.ui.ui.ui.table tr.olive:not(.marked),
-.ui.ui.table td.olive:not(.marked) {
-  background: #f7fae4;
-  color: #8ABC1E;
-}
-
-.ui.ui.selectable.table tr.olive:not(.marked):hover,
-.ui.table tr td.selectable.olive:not(.marked):hover,
-.ui.selectable.table tr:hover td.olive:not(.marked) {
-  background: #f6fada;
-  color: #8ABC1E;
-}
-
-.ui.green.table {
-  border-top: 0.2em solid #21BA45;
-}
-
-.ui.ui.ui.ui.table tr.green:not(.marked),
-.ui.ui.table td.green:not(.marked) {
-  background: #d5f5d9;
-  color: #1EBC30;
-}
-
-.ui.ui.selectable.table tr.green:not(.marked):hover,
-.ui.table tr td.selectable.green:not(.marked):hover,
-.ui.selectable.table tr:hover td.green:not(.marked) {
-  background: #d2eed5;
-  color: #1EBC30;
-}
-
-.ui.teal.table {
-  border-top: 0.2em solid #00B5AD;
-}
-
-.ui.ui.ui.ui.table tr.teal:not(.marked),
-.ui.ui.table td.teal:not(.marked) {
-  background: #e2ffff;
-  color: #10A3A3;
-}
-
-.ui.ui.selectable.table tr.teal:not(.marked):hover,
-.ui.table tr td.selectable.teal:not(.marked):hover,
-.ui.selectable.table tr:hover td.teal:not(.marked) {
-  background: #d8ffff;
-  color: #10A3A3;
-}
-
-.ui.blue.table {
-  border-top: 0.2em solid #2185D0;
-}
-
-.ui.ui.ui.ui.table tr.blue:not(.marked),
-.ui.ui.table td.blue:not(.marked) {
-  background: #ddf4ff;
-  color: #2185D0;
-}
-
-.ui.ui.selectable.table tr.blue:not(.marked):hover,
-.ui.table tr td.selectable.blue:not(.marked):hover,
-.ui.selectable.table tr:hover td.blue:not(.marked) {
-  background: #d3f1ff;
-  color: #2185D0;
-}
-
-.ui.violet.table {
-  border-top: 0.2em solid #6435C9;
-}
-
-.ui.ui.ui.ui.table tr.violet:not(.marked),
-.ui.ui.table td.violet:not(.marked) {
-  background: #ece9fe;
-  color: #6435C9;
-}
-
-.ui.ui.selectable.table tr.violet:not(.marked):hover,
-.ui.table tr td.selectable.violet:not(.marked):hover,
-.ui.selectable.table tr:hover td.violet:not(.marked) {
-  background: #e3deff;
-  color: #6435C9;
-}
-
-.ui.purple.table {
-  border-top: 0.2em solid #A333C8;
-}
-
-.ui.ui.ui.ui.table tr.purple:not(.marked),
-.ui.ui.table td.purple:not(.marked) {
-  background: #f8e3ff;
-  color: #A333C8;
-}
-
-.ui.ui.selectable.table tr.purple:not(.marked):hover,
-.ui.table tr td.selectable.purple:not(.marked):hover,
-.ui.selectable.table tr:hover td.purple:not(.marked) {
-  background: #f5d9ff;
-  color: #A333C8;
-}
-
-.ui.pink.table {
-  border-top: 0.2em solid #E03997;
-}
-
-.ui.ui.ui.ui.table tr.pink:not(.marked),
-.ui.ui.table td.pink:not(.marked) {
-  background: #ffe8f9;
-  color: #E03997;
-}
-
-.ui.ui.selectable.table tr.pink:not(.marked):hover,
-.ui.table tr td.selectable.pink:not(.marked):hover,
-.ui.selectable.table tr:hover td.pink:not(.marked) {
-  background: #ffdef6;
-  color: #E03997;
-}
-
-.ui.brown.table {
-  border-top: 0.2em solid #A5673F;
-}
-
-.ui.ui.ui.ui.table tr.brown:not(.marked),
-.ui.ui.table td.brown:not(.marked) {
-  background: #f7e5d2;
-  color: #A5673F;
-}
-
-.ui.ui.selectable.table tr.brown:not(.marked):hover,
-.ui.table tr td.selectable.brown:not(.marked):hover,
-.ui.selectable.table tr:hover td.brown:not(.marked) {
-  background: #efe0cf;
-  color: #A5673F;
-}
-
-.ui.grey.table {
-  border-top: 0.2em solid #767676;
-}
-
-.ui.ui.ui.ui.table tr.grey:not(.marked),
-.ui.ui.table td.grey:not(.marked) {
-  background: #DCDDDE;
-  color: #767676;
-}
-
-.ui.ui.selectable.table tr.grey:not(.marked):hover,
-.ui.table tr td.selectable.grey:not(.marked):hover,
-.ui.selectable.table tr:hover td.grey:not(.marked) {
-  background: #c2c4c5;
-  color: #767676;
-}
-
-.ui.black.table {
-  border-top: 0.2em solid #1B1C1D;
-}
-
-.ui.ui.ui.ui.table tr.black:not(.marked),
-.ui.ui.table td.black:not(.marked) {
-  background: #545454;
-  color: #FFFFFF;
-}
-
-.ui.ui.selectable.table tr.black:not(.marked):hover,
-.ui.table tr td.selectable.black:not(.marked):hover,
-.ui.selectable.table tr:hover td.black:not(.marked) {
-  background: #000000;
-  color: #FFFFFF;
-}
-
-/*--------------
-  Column Count
----------------*/
-
-/* Grid Based */
-
-.ui.one.column.table td {
-  width: 100%;
-}
-
-.ui.two.column.table td {
-  width: 50%;
-}
-
-.ui.three.column.table td {
-  width: 33.33333333%;
-}
-
-.ui.four.column.table td {
-  width: 25%;
-}
-
-.ui.five.column.table td {
-  width: 20%;
-}
-
-.ui.six.column.table td {
-  width: 16.66666667%;
-}
-
-.ui.seven.column.table td {
-  width: 14.28571429%;
-}
-
-.ui.eight.column.table td {
-  width: 12.5%;
-}
-
-.ui.nine.column.table td {
-  width: 11.11111111%;
-}
-
-.ui.ten.column.table td {
-  width: 10%;
-}
-
-.ui.eleven.column.table td {
-  width: 9.09090909%;
-}
-
-.ui.twelve.column.table td {
-  width: 8.33333333%;
-}
-
-.ui.thirteen.column.table td {
-  width: 7.69230769%;
-}
-
-.ui.fourteen.column.table td {
-  width: 7.14285714%;
-}
-
-.ui.fifteen.column.table td {
-  width: 6.66666667%;
-}
-
-.ui.sixteen.column.table td {
-  width: 6.25%;
-}
-
-/* Column Width */
-
-.ui.table th.one.wide,
-.ui.table td.one.wide {
-  width: 6.25%;
-}
-
-.ui.table th.two.wide,
-.ui.table td.two.wide {
-  width: 12.5%;
-}
-
-.ui.table th.three.wide,
-.ui.table td.three.wide {
-  width: 18.75%;
-}
-
-.ui.table th.four.wide,
-.ui.table td.four.wide {
-  width: 25%;
-}
-
-.ui.table th.five.wide,
-.ui.table td.five.wide {
-  width: 31.25%;
-}
-
-.ui.table th.six.wide,
-.ui.table td.six.wide {
-  width: 37.5%;
-}
-
-.ui.table th.seven.wide,
-.ui.table td.seven.wide {
-  width: 43.75%;
-}
-
-.ui.table th.eight.wide,
-.ui.table td.eight.wide {
-  width: 50%;
-}
-
-.ui.table th.nine.wide,
-.ui.table td.nine.wide {
-  width: 56.25%;
-}
-
-.ui.table th.ten.wide,
-.ui.table td.ten.wide {
-  width: 62.5%;
-}
-
-.ui.table th.eleven.wide,
-.ui.table td.eleven.wide {
-  width: 68.75%;
-}
-
-.ui.table th.twelve.wide,
-.ui.table td.twelve.wide {
-  width: 75%;
-}
-
-.ui.table th.thirteen.wide,
-.ui.table td.thirteen.wide {
-  width: 81.25%;
-}
-
-.ui.table th.fourteen.wide,
-.ui.table td.fourteen.wide {
-  width: 87.5%;
-}
-
-.ui.table th.fifteen.wide,
-.ui.table td.fifteen.wide {
-  width: 93.75%;
-}
-
-.ui.table th.sixteen.wide,
-.ui.table td.sixteen.wide {
-  width: 100%;
-}
-
-/*--------------
-      Sortable
-  ---------------*/
-
-.ui.sortable.table > thead > tr > th {
-  cursor: pointer;
-  white-space: nowrap;
-  border-left: 1px solid rgba(34, 36, 38, 0.15);
-  color: rgba(0, 0, 0, 0.87);
-}
-
-.ui.sortable.table > thead > tr > th:first-child {
-  border-left: none;
-}
-
-.ui.sortable.table thead th.sorted,
-.ui.sortable.table thead th.sorted:hover {
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  user-select: none;
-}
-
-.ui.sortable.table > thead > tr > th:after {
-  display: none;
-  font-style: normal;
-  font-weight: normal;
-  text-decoration: inherit;
-  content: '';
-  height: 1em;
-  width: auto;
-  opacity: 0.8;
-  margin: 0 0 0 0.5em;
-  font-family: 'Icons';
-}
-
-.ui.sortable.table thead th.ascending:after {
-  content: '\f0d8';
-}
-
-.ui.sortable.table thead th.descending:after {
-  content: '\f0d7';
-}
-
-/* Hover */
-
-.ui.sortable.table th.disabled:hover {
-  cursor: auto;
-  color: rgba(40, 40, 40, 0.3);
-}
-
-.ui.sortable.table > thead > tr > th:hover {
-  color: rgba(0, 0, 0, 0.8);
-}
-
-.ui.sortable.table:not(.basic) > thead > tr > th:hover {
-  background: rgba(0, 0, 0, 0.05);
-}
-
-/* Sorted */
-
-.ui.sortable.table thead th.sorted {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.sortable.table:not(.basic) thead th.sorted {
-  background: rgba(0, 0, 0, 0.05);
-}
-
-.ui.sortable.table thead th.sorted:after {
-  display: inline-block;
-}
-
-/* Sorted Hover */
-
-.ui.sortable.table thead th.sorted:hover {
-  color: rgba(0, 0, 0, 0.95);
-}
-
-.ui.sortable.table:not(.basic) thead th.sorted:hover {
-  background: rgba(0, 0, 0, 0.05);
-}
-
-/*--------------
-     Collapsing
-  ---------------*/
-
-.ui.collapsing.table {
-  width: auto;
-}
-
-/*--------------
-        Basic
-  ---------------*/
-
-.ui.basic.table {
-  background: transparent;
-  border: 1px solid rgba(34, 36, 38, 0.15);
-  box-shadow: none;
-}
-
-.ui.basic.table > thead,
-.ui.basic.table > tfoot {
-  box-shadow: none;
-}
-
-.ui.basic.table > thead > tr > th,
-.ui.basic.table > tbody > tr > th,
-.ui.basic.table > tfoot > tr > th,
-.ui.basic.table > tr > th {
-  background: transparent;
-  border-left: none;
-}
-
-.ui.basic.table > tbody > tr {
-  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
-}
-
-.ui.basic.table > tbody > tr > td,
-.ui.basic.table > tfoot > tr > td,
-.ui.basic.table > tr > td {
-  background: transparent;
-}
-
-.ui.basic.striped.table > tbody > tr:nth-child(2n) {
-  background-color: rgba(0, 0, 0, 0.05);
-}
-
-/* Very Basic */
-
-.ui[class*="very basic"].table {
-  border: none;
-}
-
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > th,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > thead > tr > th,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > th,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > th,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > td,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > td {
-  padding: '';
-}
-
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > th:first-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > thead > tr > th:first-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > th:first-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > th:first-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > td:first-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > td:first-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > td:first-child {
-  padding-left: 0;
-}
-
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > th:last-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > thead > tr > th:last-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > th:last-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > th:last-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > td:last-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > td:last-child,
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > td:last-child {
-  padding-right: 0;
-}
-
-.ui[class*="very basic"].table:not(.sortable):not(.striped) > thead > tr:first-child > th {
-  padding-top: 0;
-}
-
-/*--------------
-       Celled
-  ---------------*/
-
-.ui.celled.table > tr > th,
-.ui.celled.table > thead > tr > th,
-.ui.celled.table > tbody > tr > th,
-.ui.celled.table > tfoot > tr > th,
-.ui.celled.table > tr > td,
-.ui.celled.table > tbody > tr > td,
-.ui.celled.table > tfoot > tr > td {
-  border-left: 1px solid rgba(34, 36, 38, 0.1);
-}
-
-.ui.celled.table > tr > th:first-child,
-.ui.celled.table > thead > tr > th:first-child,
-.ui.celled.table > tbody > tr > th:first-child,
-.ui.celled.table > tfoot > tr > th:first-child,
-.ui.celled.table > tr > td:first-child,
-.ui.celled.table > tbody > tr > td:first-child,
-.ui.celled.table > tfoot > tr > td:first-child {
-  border-left: none;
-}
-
-/*--------------
-       Padded
-  ---------------*/
-
-.ui.padded.table > tr > th,
-.ui.padded.table > thead > tr > th,
-.ui.padded.table > tbody > tr > th,
-.ui.padded.table > tfoot > tr > th {
-  padding-left: 1em;
-  padding-right: 1em;
-}
-
-.ui.padded.table > tr > th,
-.ui.padded.table > thead > tr > th,
-.ui.padded.table > tbody > tr > th,
-.ui.padded.table > tfoot > tr > th,
-.ui.padded.table > tr > td,
-.ui.padded.table > tbody > tr > td,
-.ui.padded.table > tfoot > tr > td {
-  padding: 1em 1em;
-}
-
-/* Very */
-
-.ui[class*="very padded"].table > tr > th,
-.ui[class*="very padded"].table > thead > tr > th,
-.ui[class*="very padded"].table > tbody > tr > th,
-.ui[class*="very padded"].table > tfoot > tr > th {
-  padding-left: 1.5em;
-  padding-right: 1.5em;
-}
-
-.ui[class*="very padded"].table > tr > td,
-.ui[class*="very padded"].table > tbody > tr > td,
-.ui[class*="very padded"].table > tfoot > tr > td {
-  padding: 1.5em 1.5em;
-}
-
-/*--------------
-       Compact
-  ---------------*/
-
-.ui.compact.table > tr > th,
-.ui.compact.table > thead > tr > th,
-.ui.compact.table > tbody > tr > th,
-.ui.compact.table > tfoot > tr > th {
-  padding-left: 0.7em;
-  padding-right: 0.7em;
-}
-
-.ui.compact.table > tr > td,
-.ui.compact.table > tbody > tr > td,
-.ui.compact.table > tfoot > tr > td {
-  padding: 0.5em 0.7em;
-}
-
-/* Very */
-
-.ui[class*="very compact"].table > tr > th,
-.ui[class*="very compact"].table > thead > tr > th,
-.ui[class*="very compact"].table > tbody > tr > th,
-.ui[class*="very compact"].table > tfoot > tr > th {
-  padding-left: 0.6em;
-  padding-right: 0.6em;
-}
-
-.ui[class*="very compact"].table > tr > td,
-.ui[class*="very compact"].table > tbody > tr > td,
-.ui[class*="very compact"].table > tfoot > tr > td {
-  padding: 0.4em 0.6em;
-}
-
-/*--------------
-      Sizes
----------------*/
-
-/* Standard */
-
-.ui.table {
-  font-size: 1em;
-}
-
-.ui.mini.table {
-  font-size: 0.78571429rem;
-}
-
-.ui.tiny.table {
-  font-size: 0.85714286rem;
-}
-
-.ui.small.table {
-  font-size: 0.9em;
-}
-
-.ui.large.table {
-  font-size: 1.1em;
-}
-
-.ui.big.table {
-  font-size: 1.28571429rem;
-}
-
-.ui.huge.table {
-  font-size: 1.42857143rem;
-}
-
-.ui.massive.table {
-  font-size: 1.71428571rem;
-}
-
-/*******************************
-         Site Overrides
 *******************************/
\ No newline at end of file
diff --git a/web_src/fomantic/build/semantic.js b/web_src/fomantic/build/semantic.js
index 2a05d94d72..c150c8d9db 100644
--- a/web_src/fomantic/build/semantic.js
+++ b/web_src/fomantic/build/semantic.js
@@ -1184,883 +1184,6 @@ $.api.settings = {
 
 
 
-})( jQuery, window, document );
-
-/*!
- * # Fomantic-UI - Checkbox
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-;(function ($, window, document, undefined) {
-
-'use strict';
-
-$.isFunction = $.isFunction || function(obj) {
-  return typeof obj === "function" && typeof obj.nodeType !== "number";
-};
-
-window = (typeof window != 'undefined' && window.Math == Math)
-  ? window
-  : (typeof self != 'undefined' && self.Math == Math)
-    ? self
-    : Function('return this')()
-;
-
-$.fn.checkbox = function(parameters) {
-  var
-    $allModules    = $(this),
-    moduleSelector = $allModules.selector || '',
-
-    time           = new Date().getTime(),
-    performance    = [],
-
-    query          = arguments[0],
-    methodInvoked  = (typeof query == 'string'),
-    queryArguments = [].slice.call(arguments, 1),
-    returnedValue
-  ;
-
-  $allModules
-    .each(function() {
-      var
-        settings        = $.extend(true, {}, $.fn.checkbox.settings, parameters),
-
-        className       = settings.className,
-        namespace       = settings.namespace,
-        selector        = settings.selector,
-        error           = settings.error,
-
-        eventNamespace  = '.' + namespace,
-        moduleNamespace = 'module-' + namespace,
-
-        $module         = $(this),
-        $label          = $(this).children(selector.label),
-        $input          = $(this).children(selector.input),
-        input           = $input[0],
-
-        initialLoad     = false,
-        shortcutPressed = false,
-        instance        = $module.data(moduleNamespace),
-
-        observer,
-        element         = this,
-        module
-      ;
-
-      module      = {
-
-        initialize: function() {
-          module.verbose('Initializing checkbox', settings);
-
-          module.create.label();
-          module.bind.events();
-
-          module.set.tabbable();
-          module.hide.input();
-
-          module.observeChanges();
-          module.instantiate();
-          module.setup();
-        },
-
-        instantiate: function() {
-          module.verbose('Storing instance of module', module);
-          instance = module;
-          $module
-            .data(moduleNamespace, module)
-          ;
-        },
-
-        destroy: function() {
-          module.verbose('Destroying module');
-          module.unbind.events();
-          module.show.input();
-          $module.removeData(moduleNamespace);
-        },
-
-        fix: {
-          reference: function() {
-            if( $module.is(selector.input) ) {
-              module.debug('Behavior called on <input> adjusting invoked element');
-              $module = $module.closest(selector.checkbox);
-              module.refresh();
-            }
-          }
-        },
-
-        setup: function() {
-          module.set.initialLoad();
-          if( module.is.indeterminate() ) {
-            module.debug('Initial value is indeterminate');
-            module.indeterminate();
-          }
-          else if( module.is.checked() ) {
-            module.debug('Initial value is checked');
-            module.check();
-          }
-          else {
-            module.debug('Initial value is unchecked');
-            module.uncheck();
-          }
-          module.remove.initialLoad();
-        },
-
-        refresh: function() {
-          $label = $module.children(selector.label);
-          $input = $module.children(selector.input);
-          input  = $input[0];
-        },
-
-        hide: {
-          input: function() {
-            module.verbose('Modifying <input> z-index to be unselectable');
-            $input.addClass(className.hidden);
-          }
-        },
-        show: {
-          input: function() {
-            module.verbose('Modifying <input> z-index to be selectable');
-            $input.removeClass(className.hidden);
-          }
-        },
-
-        observeChanges: function() {
-          if('MutationObserver' in window) {
-            observer = new MutationObserver(function(mutations) {
-              module.debug('DOM tree modified, updating selector cache');
-              module.refresh();
-            });
-            observer.observe(element, {
-              childList : true,
-              subtree   : true
-            });
-            module.debug('Setting up mutation observer', observer);
-          }
-        },
-
-        attachEvents: function(selector, event) {
-          var
-            $element = $(selector)
-          ;
-          event = $.isFunction(module[event])
-            ? module[event]
-            : module.toggle
-          ;
-          if($element.length > 0) {
-            module.debug('Attaching checkbox events to element', selector, event);
-            $element
-              .on('click' + eventNamespace, event)
-            ;
-          }
-          else {
-            module.error(error.notFound);
-          }
-        },
-
-        preventDefaultOnInputTarget: function() {
-          if(typeof event !== 'undefined' && event !== null && $(event.target).is(selector.input)) {
-            module.verbose('Preventing default check action after manual check action');
-            event.preventDefault();
-          }
-        },
-
-        event: {
-          change: function(event) {
-            if( !module.should.ignoreCallbacks() ) {
-              settings.onChange.call(input);
-            }
-          },
-          click: function(event) {
-            var
-              $target = $(event.target)
-            ;
-            if( $target.is(selector.input) ) {
-              module.verbose('Using default check action on initialized checkbox');
-              return;
-            }
-            if( $target.is(selector.link) ) {
-              module.debug('Clicking link inside checkbox, skipping toggle');
-              return;
-            }
-            module.toggle();
-            $input.focus();
-            event.preventDefault();
-          },
-          keydown: function(event) {
-            var
-              key     = event.which,
-              keyCode = {
-                enter  : 13,
-                space  : 32,
-                escape : 27,
-                left   : 37,
-                up     : 38,
-                right  : 39,
-                down   : 40
-              }
-            ;
-
-            var r = module.get.radios(),
-                rIndex = r.index($module),
-                rLen = r.length,
-                checkIndex = false;
-
-            if(key == keyCode.left || key == keyCode.up) {
-              checkIndex = (rIndex === 0 ? rLen : rIndex) - 1;
-            } else if(key == keyCode.right || key == keyCode.down) {
-              checkIndex = rIndex === rLen-1 ? 0 : rIndex+1;
-            }
-
-            if (!module.should.ignoreCallbacks() && checkIndex !== false) {
-              if(settings.beforeUnchecked.apply(input)===false) {
-                module.verbose('Option not allowed to be unchecked, cancelling key navigation');
-                return false;
-              }
-              if (settings.beforeChecked.apply($(r[checkIndex]).children(selector.input)[0])===false) {
-                module.verbose('Next option should not allow check, cancelling key navigation');
-                return false;
-              }
-            }
-
-            if(key == keyCode.escape) {
-              module.verbose('Escape key pressed blurring field');
-              $input.blur();
-              shortcutPressed = true;
-            }
-            else if(!event.ctrlKey && ( key == keyCode.space || (key == keyCode.enter && settings.enableEnterKey)) ) {
-              module.verbose('Enter/space key pressed, toggling checkbox');
-              module.toggle();
-              shortcutPressed = true;
-            }
-            else {
-              shortcutPressed = false;
-            }
-          },
-          keyup: function(event) {
-            if(shortcutPressed) {
-              event.preventDefault();
-            }
-          }
-        },
-
-        check: function() {
-          if( !module.should.allowCheck() ) {
-            return;
-          }
-          module.debug('Checking checkbox', $input);
-          module.set.checked();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onChecked.call(input);
-            module.trigger.change();
-          }
-          module.preventDefaultOnInputTarget();
-        },
-
-        uncheck: function() {
-          if( !module.should.allowUncheck() ) {
-            return;
-          }
-          module.debug('Unchecking checkbox');
-          module.set.unchecked();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onUnchecked.call(input);
-            module.trigger.change();
-          }
-          module.preventDefaultOnInputTarget();
-        },
-
-        indeterminate: function() {
-          if( module.should.allowIndeterminate() ) {
-            module.debug('Checkbox is already indeterminate');
-            return;
-          }
-          module.debug('Making checkbox indeterminate');
-          module.set.indeterminate();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onIndeterminate.call(input);
-            module.trigger.change();
-          }
-        },
-
-        determinate: function() {
-          if( module.should.allowDeterminate() ) {
-            module.debug('Checkbox is already determinate');
-            return;
-          }
-          module.debug('Making checkbox determinate');
-          module.set.determinate();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onDeterminate.call(input);
-            module.trigger.change();
-          }
-        },
-
-        enable: function() {
-          if( module.is.enabled() ) {
-            module.debug('Checkbox is already enabled');
-            return;
-          }
-          module.debug('Enabling checkbox');
-          module.set.enabled();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onEnable.call(input);
-            // preserve legacy callbacks
-            settings.onEnabled.call(input);
-            module.trigger.change();
-          }
-        },
-
-        disable: function() {
-          if( module.is.disabled() ) {
-            module.debug('Checkbox is already disabled');
-            return;
-          }
-          module.debug('Disabling checkbox');
-          module.set.disabled();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onDisable.call(input);
-            // preserve legacy callbacks
-            settings.onDisabled.call(input);
-            module.trigger.change();
-          }
-        },
-
-        get: {
-          radios: function() {
-            var
-              name = module.get.name()
-            ;
-            return $('input[name="' + name + '"]').closest(selector.checkbox);
-          },
-          otherRadios: function() {
-            return module.get.radios().not($module);
-          },
-          name: function() {
-            return $input.attr('name');
-          }
-        },
-
-        is: {
-          initialLoad: function() {
-            return initialLoad;
-          },
-          radio: function() {
-            return ($input.hasClass(className.radio) || $input.attr('type') == 'radio');
-          },
-          indeterminate: function() {
-            return $input.prop('indeterminate') !== undefined && $input.prop('indeterminate');
-          },
-          checked: function() {
-            return $input.prop('checked') !== undefined && $input.prop('checked');
-          },
-          disabled: function() {
-            return $input.prop('disabled') !== undefined && $input.prop('disabled');
-          },
-          enabled: function() {
-            return !module.is.disabled();
-          },
-          determinate: function() {
-            return !module.is.indeterminate();
-          },
-          unchecked: function() {
-            return !module.is.checked();
-          }
-        },
-
-        should: {
-          allowCheck: function() {
-            if(module.is.determinate() && module.is.checked() && !module.is.initialLoad() ) {
-              module.debug('Should not allow check, checkbox is already checked');
-              return false;
-            }
-            if(!module.should.ignoreCallbacks() && settings.beforeChecked.apply(input) === false) {
-              module.debug('Should not allow check, beforeChecked cancelled');
-              return false;
-            }
-            return true;
-          },
-          allowUncheck: function() {
-            if(module.is.determinate() && module.is.unchecked() && !module.is.initialLoad() ) {
-              module.debug('Should not allow uncheck, checkbox is already unchecked');
-              return false;
-            }
-            if(!module.should.ignoreCallbacks() && settings.beforeUnchecked.apply(input) === false) {
-              module.debug('Should not allow uncheck, beforeUnchecked cancelled');
-              return false;
-            }
-            return true;
-          },
-          allowIndeterminate: function() {
-            if(module.is.indeterminate() && !module.is.initialLoad() ) {
-              module.debug('Should not allow indeterminate, checkbox is already indeterminate');
-              return false;
-            }
-            if(!module.should.ignoreCallbacks() && settings.beforeIndeterminate.apply(input) === false) {
-              module.debug('Should not allow indeterminate, beforeIndeterminate cancelled');
-              return false;
-            }
-            return true;
-          },
-          allowDeterminate: function() {
-            if(module.is.determinate() && !module.is.initialLoad() ) {
-              module.debug('Should not allow determinate, checkbox is already determinate');
-              return false;
-            }
-            if(!module.should.ignoreCallbacks() && settings.beforeDeterminate.apply(input) === false) {
-              module.debug('Should not allow determinate, beforeDeterminate cancelled');
-              return false;
-            }
-            return true;
-          },
-          ignoreCallbacks: function() {
-            return (initialLoad && !settings.fireOnInit);
-          }
-        },
-
-        can: {
-          change: function() {
-            return !( $module.hasClass(className.disabled) || $module.hasClass(className.readOnly) || $input.prop('disabled') || $input.prop('readonly') );
-          },
-          uncheck: function() {
-            return (typeof settings.uncheckable === 'boolean')
-              ? settings.uncheckable
-              : !module.is.radio()
-            ;
-          }
-        },
-
-        set: {
-          initialLoad: function() {
-            initialLoad = true;
-          },
-          checked: function() {
-            module.verbose('Setting class to checked');
-            $module
-              .removeClass(className.indeterminate)
-              .addClass(className.checked)
-            ;
-            if( module.is.radio() ) {
-              module.uncheckOthers();
-            }
-            if(!module.is.indeterminate() && module.is.checked()) {
-              module.debug('Input is already checked, skipping input property change');
-              return;
-            }
-            module.verbose('Setting state to checked', input);
-            $input
-              .prop('indeterminate', false)
-              .prop('checked', true)
-            ;
-          },
-          unchecked: function() {
-            module.verbose('Removing checked class');
-            $module
-              .removeClass(className.indeterminate)
-              .removeClass(className.checked)
-            ;
-            if(!module.is.indeterminate() &&  module.is.unchecked() ) {
-              module.debug('Input is already unchecked');
-              return;
-            }
-            module.debug('Setting state to unchecked');
-            $input
-              .prop('indeterminate', false)
-              .prop('checked', false)
-            ;
-          },
-          indeterminate: function() {
-            module.verbose('Setting class to indeterminate');
-            $module
-              .addClass(className.indeterminate)
-            ;
-            if( module.is.indeterminate() ) {
-              module.debug('Input is already indeterminate, skipping input property change');
-              return;
-            }
-            module.debug('Setting state to indeterminate');
-            $input
-              .prop('indeterminate', true)
-            ;
-          },
-          determinate: function() {
-            module.verbose('Removing indeterminate class');
-            $module
-              .removeClass(className.indeterminate)
-            ;
-            if( module.is.determinate() ) {
-              module.debug('Input is already determinate, skipping input property change');
-              return;
-            }
-            module.debug('Setting state to determinate');
-            $input
-              .prop('indeterminate', false)
-            ;
-          },
-          disabled: function() {
-            module.verbose('Setting class to disabled');
-            $module
-              .addClass(className.disabled)
-            ;
-            if( module.is.disabled() ) {
-              module.debug('Input is already disabled, skipping input property change');
-              return;
-            }
-            module.debug('Setting state to disabled');
-            $input
-              .prop('disabled', 'disabled')
-            ;
-          },
-          enabled: function() {
-            module.verbose('Removing disabled class');
-            $module.removeClass(className.disabled);
-            if( module.is.enabled() ) {
-              module.debug('Input is already enabled, skipping input property change');
-              return;
-            }
-            module.debug('Setting state to enabled');
-            $input
-              .prop('disabled', false)
-            ;
-          },
-          tabbable: function() {
-            module.verbose('Adding tabindex to checkbox');
-            if( $input.attr('tabindex') === undefined) {
-              $input.attr('tabindex', 0);
-            }
-          }
-        },
-
-        remove: {
-          initialLoad: function() {
-            initialLoad = false;
-          }
-        },
-
-        trigger: {
-          change: function() {
-            var
-              inputElement = $input[0]
-            ;
-            if(inputElement) {
-              var events = document.createEvent('HTMLEvents');
-              module.verbose('Triggering native change event');
-              events.initEvent('change', true, false);
-              inputElement.dispatchEvent(events);
-            }
-          }
-        },
-
-
-        create: {
-          label: function() {
-            if($input.prevAll(selector.label).length > 0) {
-              $input.prev(selector.label).detach().insertAfter($input);
-              module.debug('Moving existing label', $label);
-            }
-            else if( !module.has.label() ) {
-              $label = $('<label>').insertAfter($input);
-              module.debug('Creating label', $label);
-            }
-          }
-        },
-
-        has: {
-          label: function() {
-            return ($label.length > 0);
-          }
-        },
-
-        bind: {
-          events: function() {
-            module.verbose('Attaching checkbox events');
-            $module
-              .on('click'   + eventNamespace, module.event.click)
-              .on('change'  + eventNamespace, module.event.change)
-              .on('keydown' + eventNamespace, selector.input, module.event.keydown)
-              .on('keyup'   + eventNamespace, selector.input, module.event.keyup)
-            ;
-          }
-        },
-
-        unbind: {
-          events: function() {
-            module.debug('Removing events');
-            $module
-              .off(eventNamespace)
-            ;
-          }
-        },
-
-        uncheckOthers: function() {
-          var
-            $radios = module.get.otherRadios()
-          ;
-          module.debug('Unchecking other radios', $radios);
-          $radios.removeClass(className.checked);
-        },
-
-        toggle: function() {
-          if( !module.can.change() ) {
-            if(!module.is.radio()) {
-              module.debug('Checkbox is read-only or disabled, ignoring toggle');
-            }
-            return;
-          }
-          if( module.is.indeterminate() || module.is.unchecked() ) {
-            module.debug('Currently unchecked');
-            module.check();
-          }
-          else if( module.is.checked() && module.can.uncheck() ) {
-            module.debug('Currently checked');
-            module.uncheck();
-          }
-        },
-        setting: function(name, value) {
-          module.debug('Changing setting', name, value);
-          if( $.isPlainObject(name) ) {
-            $.extend(true, settings, name);
-          }
-          else if(value !== undefined) {
-            if($.isPlainObject(settings[name])) {
-              $.extend(true, settings[name], value);
-            }
-            else {
-              settings[name] = value;
-            }
-          }
-          else {
-            return settings[name];
-          }
-        },
-        internal: function(name, value) {
-          if( $.isPlainObject(name) ) {
-            $.extend(true, module, name);
-          }
-          else if(value !== undefined) {
-            module[name] = value;
-          }
-          else {
-            return module[name];
-          }
-        },
-        debug: function() {
-          if(!settings.silent && settings.debug) {
-            if(settings.performance) {
-              module.performance.log(arguments);
-            }
-            else {
-              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
-              module.debug.apply(console, arguments);
-            }
-          }
-        },
-        verbose: function() {
-          if(!settings.silent && settings.verbose && settings.debug) {
-            if(settings.performance) {
-              module.performance.log(arguments);
-            }
-            else {
-              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
-              module.verbose.apply(console, arguments);
-            }
-          }
-        },
-        error: function() {
-          if(!settings.silent) {
-            module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
-            module.error.apply(console, arguments);
-          }
-        },
-        performance: {
-          log: function(message) {
-            var
-              currentTime,
-              executionTime,
-              previousTime
-            ;
-            if(settings.performance) {
-              currentTime   = new Date().getTime();
-              previousTime  = time || currentTime;
-              executionTime = currentTime - previousTime;
-              time          = currentTime;
-              performance.push({
-                'Name'           : message[0],
-                'Arguments'      : [].slice.call(message, 1) || '',
-                'Element'        : element,
-                'Execution Time' : executionTime
-              });
-            }
-            clearTimeout(module.performance.timer);
-            module.performance.timer = setTimeout(module.performance.display, 500);
-          },
-          display: function() {
-            var
-              title = settings.name + ':',
-              totalTime = 0
-            ;
-            time = false;
-            clearTimeout(module.performance.timer);
-            $.each(performance, function(index, data) {
-              totalTime += data['Execution Time'];
-            });
-            title += ' ' + totalTime + 'ms';
-            if(moduleSelector) {
-              title += ' \'' + moduleSelector + '\'';
-            }
-            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
-              console.groupCollapsed(title);
-              if(console.table) {
-                console.table(performance);
-              }
-              else {
-                $.each(performance, function(index, data) {
-                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
-                });
-              }
-              console.groupEnd();
-            }
-            performance = [];
-          }
-        },
-        invoke: function(query, passedArguments, context) {
-          var
-            object = instance,
-            maxDepth,
-            found,
-            response
-          ;
-          passedArguments = passedArguments || queryArguments;
-          context         = element         || context;
-          if(typeof query == 'string' && object !== undefined) {
-            query    = query.split(/[\. ]/);
-            maxDepth = query.length - 1;
-            $.each(query, function(depth, value) {
-              var camelCaseValue = (depth != maxDepth)
-                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
-                : query
-              ;
-              if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
-                object = object[camelCaseValue];
-              }
-              else if( object[camelCaseValue] !== undefined ) {
-                found = object[camelCaseValue];
-                return false;
-              }
-              else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
-                object = object[value];
-              }
-              else if( object[value] !== undefined ) {
-                found = object[value];
-                return false;
-              }
-              else {
-                module.error(error.method, query);
-                return false;
-              }
-            });
-          }
-          if ( $.isFunction( found ) ) {
-            response = found.apply(context, passedArguments);
-          }
-          else if(found !== undefined) {
-            response = found;
-          }
-          if(Array.isArray(returnedValue)) {
-            returnedValue.push(response);
-          }
-          else if(returnedValue !== undefined) {
-            returnedValue = [returnedValue, response];
-          }
-          else if(response !== undefined) {
-            returnedValue = response;
-          }
-          return found;
-        }
-      };
-
-      if(methodInvoked) {
-        if(instance === undefined) {
-          module.initialize();
-        }
-        module.invoke(query);
-      }
-      else {
-        if(instance !== undefined) {
-          instance.invoke('destroy');
-        }
-        module.initialize();
-      }
-    })
-  ;
-
-  return (returnedValue !== undefined)
-    ? returnedValue
-    : this
-  ;
-};
-
-$.fn.checkbox.settings = {
-
-  name                : 'Checkbox',
-  namespace           : 'checkbox',
-
-  silent              : false,
-  debug               : false,
-  verbose             : true,
-  performance         : true,
-
-  // delegated event context
-  uncheckable         : 'auto',
-  fireOnInit          : false,
-  enableEnterKey      : true,
-
-  onChange            : function(){},
-
-  beforeChecked       : function(){},
-  beforeUnchecked     : function(){},
-  beforeDeterminate   : function(){},
-  beforeIndeterminate : function(){},
-
-  onChecked           : function(){},
-  onUnchecked         : function(){},
-
-  onDeterminate       : function() {},
-  onIndeterminate     : function() {},
-
-  onEnable            : function(){},
-  onDisable           : function(){},
-
-  // preserve misspelled callbacks (will be removed in 3.0)
-  onEnabled           : function(){},
-  onDisabled          : function(){},
-
-  className       : {
-    checked       : 'checked',
-    indeterminate : 'indeterminate',
-    disabled      : 'disabled',
-    hidden        : 'hidden',
-    radio         : 'radio',
-    readOnly      : 'read-only'
-  },
-
-  error     : {
-    method       : 'The method you called is not defined'
-  },
-
-  selector : {
-    checkbox : '.ui.checkbox',
-    label    : 'label, .box',
-    input    : 'input[type="checkbox"], input[type="radio"]',
-    link     : 'a[href]'
-  }
-
-};
-
 })( jQuery, window, document );
 
 /*!
@@ -11864,500 +10987,6 @@ $.fn.search.settings = {
   }
 };
 
-})( jQuery, window, document );
-
-/*!
- * # Fomantic-UI - Site
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-;(function ($, window, document, undefined) {
-
-$.isFunction = $.isFunction || function(obj) {
-    return typeof obj === "function" && typeof obj.nodeType !== "number";
-};
-
-$.site = $.fn.site = function(parameters) {
-  var
-    time           = new Date().getTime(),
-    performance    = [],
-
-    query          = arguments[0],
-    methodInvoked  = (typeof query == 'string'),
-    queryArguments = [].slice.call(arguments, 1),
-
-    settings        = ( $.isPlainObject(parameters) )
-      ? $.extend(true, {}, $.site.settings, parameters)
-      : $.extend({}, $.site.settings),
-
-    namespace       = settings.namespace,
-    error           = settings.error,
-
-    moduleNamespace = 'module-' + namespace,
-
-    $document       = $(document),
-    $module         = $document,
-    element         = this,
-    instance        = $module.data(moduleNamespace),
-
-    module,
-    returnedValue
-  ;
-  module = {
-
-    initialize: function() {
-      module.instantiate();
-    },
-
-    instantiate: function() {
-      module.verbose('Storing instance of site', module);
-      instance = module;
-      $module
-        .data(moduleNamespace, module)
-      ;
-    },
-
-    normalize: function() {
-      module.fix.console();
-      module.fix.requestAnimationFrame();
-    },
-
-    fix: {
-      console: function() {
-        module.debug('Normalizing window.console');
-        if (console === undefined || console.log === undefined) {
-          module.verbose('Console not available, normalizing events');
-          module.disable.console();
-        }
-        if (typeof console.group == 'undefined' || typeof console.groupEnd == 'undefined' || typeof console.groupCollapsed == 'undefined') {
-          module.verbose('Console group not available, normalizing events');
-          window.console.group = function() {};
-          window.console.groupEnd = function() {};
-          window.console.groupCollapsed = function() {};
-        }
-        if (typeof console.markTimeline == 'undefined') {
-          module.verbose('Mark timeline not available, normalizing events');
-          window.console.markTimeline = function() {};
-        }
-      },
-      consoleClear: function() {
-        module.debug('Disabling programmatic console clearing');
-        window.console.clear = function() {};
-      },
-      requestAnimationFrame: function() {
-        module.debug('Normalizing requestAnimationFrame');
-        if(window.requestAnimationFrame === undefined) {
-          module.debug('RequestAnimationFrame not available, normalizing event');
-          window.requestAnimationFrame = window.requestAnimationFrame
-            || window.mozRequestAnimationFrame
-            || window.webkitRequestAnimationFrame
-            || window.msRequestAnimationFrame
-            || function(callback) { setTimeout(callback, 0); }
-          ;
-        }
-      }
-    },
-
-    moduleExists: function(name) {
-      return ($.fn[name] !== undefined && $.fn[name].settings !== undefined);
-    },
-
-    enabled: {
-      modules: function(modules) {
-        var
-          enabledModules = []
-        ;
-        modules = modules || settings.modules;
-        $.each(modules, function(index, name) {
-          if(module.moduleExists(name)) {
-            enabledModules.push(name);
-          }
-        });
-        return enabledModules;
-      }
-    },
-
-    disabled: {
-      modules: function(modules) {
-        var
-          disabledModules = []
-        ;
-        modules = modules || settings.modules;
-        $.each(modules, function(index, name) {
-          if(!module.moduleExists(name)) {
-            disabledModules.push(name);
-          }
-        });
-        return disabledModules;
-      }
-    },
-
-    change: {
-      setting: function(setting, value, modules, modifyExisting) {
-        modules = (typeof modules === 'string')
-          ? (modules === 'all')
-            ? settings.modules
-            : [modules]
-          : modules || settings.modules
-        ;
-        modifyExisting = (modifyExisting !== undefined)
-          ? modifyExisting
-          : true
-        ;
-        $.each(modules, function(index, name) {
-          var
-            namespace = (module.moduleExists(name))
-              ? $.fn[name].settings.namespace || false
-              : true,
-            $existingModules
-          ;
-          if(module.moduleExists(name)) {
-            module.verbose('Changing default setting', setting, value, name);
-            $.fn[name].settings[setting] = value;
-            if(modifyExisting && namespace) {
-              $existingModules = $(':data(module-' + namespace + ')');
-              if($existingModules.length > 0) {
-                module.verbose('Modifying existing settings', $existingModules);
-                $existingModules[name]('setting', setting, value);
-              }
-            }
-          }
-        });
-      },
-      settings: function(newSettings, modules, modifyExisting) {
-        modules = (typeof modules === 'string')
-          ? [modules]
-          : modules || settings.modules
-        ;
-        modifyExisting = (modifyExisting !== undefined)
-          ? modifyExisting
-          : true
-        ;
-        $.each(modules, function(index, name) {
-          var
-            $existingModules
-          ;
-          if(module.moduleExists(name)) {
-            module.verbose('Changing default setting', newSettings, name);
-            $.extend(true, $.fn[name].settings, newSettings);
-            if(modifyExisting && namespace) {
-              $existingModules = $(':data(module-' + namespace + ')');
-              if($existingModules.length > 0) {
-                module.verbose('Modifying existing settings', $existingModules);
-                $existingModules[name]('setting', newSettings);
-              }
-            }
-          }
-        });
-      }
-    },
-
-    enable: {
-      console: function() {
-        module.console(true);
-      },
-      debug: function(modules, modifyExisting) {
-        modules = modules || settings.modules;
-        module.debug('Enabling debug for modules', modules);
-        module.change.setting('debug', true, modules, modifyExisting);
-      },
-      verbose: function(modules, modifyExisting) {
-        modules = modules || settings.modules;
-        module.debug('Enabling verbose debug for modules', modules);
-        module.change.setting('verbose', true, modules, modifyExisting);
-      }
-    },
-    disable: {
-      console: function() {
-        module.console(false);
-      },
-      debug: function(modules, modifyExisting) {
-        modules = modules || settings.modules;
-        module.debug('Disabling debug for modules', modules);
-        module.change.setting('debug', false, modules, modifyExisting);
-      },
-      verbose: function(modules, modifyExisting) {
-        modules = modules || settings.modules;
-        module.debug('Disabling verbose debug for modules', modules);
-        module.change.setting('verbose', false, modules, modifyExisting);
-      }
-    },
-
-    console: function(enable) {
-      if(enable) {
-        if(instance.cache.console === undefined) {
-          module.error(error.console);
-          return;
-        }
-        module.debug('Restoring console function');
-        window.console = instance.cache.console;
-      }
-      else {
-        module.debug('Disabling console function');
-        instance.cache.console = window.console;
-        window.console = {
-          clear          : function(){},
-          error          : function(){},
-          group          : function(){},
-          groupCollapsed : function(){},
-          groupEnd       : function(){},
-          info           : function(){},
-          log            : function(){},
-          markTimeline   : function(){},
-          warn           : function(){}
-        };
-      }
-    },
-
-    destroy: function() {
-      module.verbose('Destroying previous site for', $module);
-      $module
-        .removeData(moduleNamespace)
-      ;
-    },
-
-    cache: {},
-
-    setting: function(name, value) {
-      if( $.isPlainObject(name) ) {
-        $.extend(true, settings, name);
-      }
-      else if(value !== undefined) {
-        settings[name] = value;
-      }
-      else {
-        return settings[name];
-      }
-    },
-    internal: function(name, value) {
-      if( $.isPlainObject(name) ) {
-        $.extend(true, module, name);
-      }
-      else if(value !== undefined) {
-        module[name] = value;
-      }
-      else {
-        return module[name];
-      }
-    },
-    debug: function() {
-      if(settings.debug) {
-        if(settings.performance) {
-          module.performance.log(arguments);
-        }
-        else {
-          module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
-          module.debug.apply(console, arguments);
-        }
-      }
-    },
-    verbose: function() {
-      if(settings.verbose && settings.debug) {
-        if(settings.performance) {
-          module.performance.log(arguments);
-        }
-        else {
-          module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
-          module.verbose.apply(console, arguments);
-        }
-      }
-    },
-    error: function() {
-      module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
-      module.error.apply(console, arguments);
-    },
-    performance: {
-      log: function(message) {
-        var
-          currentTime,
-          executionTime,
-          previousTime
-        ;
-        if(settings.performance) {
-          currentTime   = new Date().getTime();
-          previousTime  = time || currentTime;
-          executionTime = currentTime - previousTime;
-          time          = currentTime;
-          performance.push({
-            'Element'        : element,
-            'Name'           : message[0],
-            'Arguments'      : [].slice.call(message, 1) || '',
-            'Execution Time' : executionTime
-          });
-        }
-        clearTimeout(module.performance.timer);
-        module.performance.timer = setTimeout(module.performance.display, 500);
-      },
-      display: function() {
-        var
-          title = settings.name + ':',
-          totalTime = 0
-        ;
-        time = false;
-        clearTimeout(module.performance.timer);
-        $.each(performance, function(index, data) {
-          totalTime += data['Execution Time'];
-        });
-        title += ' ' + totalTime + 'ms';
-        if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
-          console.groupCollapsed(title);
-          if(console.table) {
-            console.table(performance);
-          }
-          else {
-            $.each(performance, function(index, data) {
-              console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
-            });
-          }
-          console.groupEnd();
-        }
-        performance = [];
-      }
-    },
-    invoke: function(query, passedArguments, context) {
-      var
-        object = instance,
-        maxDepth,
-        found,
-        response
-      ;
-      passedArguments = passedArguments || queryArguments;
-      context         = element         || context;
-      if(typeof query == 'string' && object !== undefined) {
-        query    = query.split(/[\. ]/);
-        maxDepth = query.length - 1;
-        $.each(query, function(depth, value) {
-          var camelCaseValue = (depth != maxDepth)
-            ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
-            : query
-          ;
-          if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
-            object = object[camelCaseValue];
-          }
-          else if( object[camelCaseValue] !== undefined ) {
-            found = object[camelCaseValue];
-            return false;
-          }
-          else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
-            object = object[value];
-          }
-          else if( object[value] !== undefined ) {
-            found = object[value];
-            return false;
-          }
-          else {
-            module.error(error.method, query);
-            return false;
-          }
-        });
-      }
-      if ( $.isFunction( found ) ) {
-        response = found.apply(context, passedArguments);
-      }
-      else if(found !== undefined) {
-        response = found;
-      }
-      if(Array.isArray(returnedValue)) {
-        returnedValue.push(response);
-      }
-      else if(returnedValue !== undefined) {
-        returnedValue = [returnedValue, response];
-      }
-      else if(response !== undefined) {
-        returnedValue = response;
-      }
-      return found;
-    }
-  };
-
-  if(methodInvoked) {
-    if(instance === undefined) {
-      module.initialize();
-    }
-    module.invoke(query);
-  }
-  else {
-    if(instance !== undefined) {
-      module.destroy();
-    }
-    module.initialize();
-  }
-  return (returnedValue !== undefined)
-    ? returnedValue
-    : this
-  ;
-};
-
-$.site.settings = {
-
-  name        : 'Site',
-  namespace   : 'site',
-
-  error : {
-    console : 'Console cannot be restored, most likely it was overwritten outside of module',
-    method : 'The method you called is not defined.'
-  },
-
-  debug       : false,
-  verbose     : false,
-  performance : true,
-
-  modules: [
-    'accordion',
-    'api',
-    'calendar',
-    'checkbox',
-    'dimmer',
-    'dropdown',
-    'embed',
-    'form',
-    'modal',
-    'nag',
-    'popup',
-    'slider',
-    'rating',
-    'shape',
-    'sidebar',
-    'state',
-    'sticky',
-    'tab',
-    'toast',
-    'transition',
-    'visibility',
-    'visit'
-  ],
-
-  siteNamespace   : 'site',
-  namespaceStub   : {
-    cache     : {},
-    config    : {},
-    sections  : {},
-    section   : {},
-    utilities : {}
-  }
-
-};
-
-// allows for selection of elements with data attributes
-$.extend($.expr[ ":" ], {
-  data: ($.expr.createPseudo)
-    ? $.expr.createPseudo(function(dataName) {
-        return function(elem) {
-          return !!$.data(elem, dataName);
-        };
-      })
-    : function(elem, i, match) {
-      // support: jQuery < 1.8
-      return !!$.data(elem, match[ 3 ]);
-    }
-});
-
-
 })( jQuery, window, document );
 
 /*!
diff --git a/web_src/fomantic/package-lock.json b/web_src/fomantic/package-lock.json
index 8283122eb3..4000d10da7 100644
--- a/web_src/fomantic/package-lock.json
+++ b/web_src/fomantic/package-lock.json
@@ -19,6 +19,100 @@
         "findup": "bin/findup.js"
       }
     },
+    "node_modules/@choojs/findup/node_modules/commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+    },
+    "node_modules/@isaacs/cliui/node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
     "node_modules/@octokit/auth-token": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
@@ -28,35 +122,97 @@
       }
     },
     "node_modules/@octokit/core": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz",
-      "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.0.1.tgz",
+      "integrity": "sha512-MIpPQXu8Y8GjHwXM81JLveiV+DHJZtLMcB5nKekBGOl3iAtk0HT3i12Xl8Biybu+bCS1+k4qbuKEq5d0RxNRnQ==",
       "peer": true,
       "dependencies": {
-        "@octokit/auth-token": "^2.4.4",
-        "@octokit/graphql": "^4.5.8",
-        "@octokit/request": "^5.6.3",
-        "@octokit/request-error": "^2.0.5",
-        "@octokit/types": "^6.0.3",
-        "before-after-hook": "^2.2.0",
-        "universal-user-agent": "^6.0.0"
+        "@octokit/auth-token": "^5.0.0",
+        "@octokit/graphql": "^8.0.0",
+        "@octokit/request": "^9.0.0",
+        "@octokit/request-error": "^6.0.1",
+        "@octokit/types": "^12.0.0",
+        "before-after-hook": "^3.0.2",
+        "universal-user-agent": "^7.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/core/node_modules/@octokit/auth-token": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.0.1.tgz",
+      "integrity": "sha512-RTmWsLfig8SBoiSdgvCht4BXl1CHU89Co5xiQ5JF19my/sIRDFCQ1RPrmK0exgqUZuNm39C/bV8+/83+MJEjGg==",
+      "peer": true,
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/core/node_modules/@octokit/endpoint": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.0.0.tgz",
+      "integrity": "sha512-emBcNDxBdC1y3+knJonS5zhUB/CG6TihubxM2U1/pG/Z1y3a4oV0Gzz3lmkCvWWQI6h3tqBAX9MgCBFp+M68Jw==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/types": "^12.0.0",
+        "universal-user-agent": "^7.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/core/node_modules/@octokit/openapi-types": {
+      "version": "20.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
+      "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
+      "peer": true
+    },
+    "node_modules/@octokit/core/node_modules/@octokit/request": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.0.1.tgz",
+      "integrity": "sha512-kL+cAcbSl3dctYLuJmLfx6Iku2MXXy0jszhaEIjQNaCp4zjHXrhVAHeuaRdNvJjW9qjl3u1MJ72+OuBP0YW/pg==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/endpoint": "^10.0.0",
+        "@octokit/request-error": "^6.0.1",
+        "@octokit/types": "^12.0.0",
+        "universal-user-agent": "^7.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
       }
     },
     "node_modules/@octokit/core/node_modules/@octokit/request-error": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
-      "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.0.2.tgz",
+      "integrity": "sha512-WtRVpoHcNXs84+s9s/wqfHaxM68NGMg8Av7h59B50OVO0PwwMx+2GgQ/OliUd0iQBSNWgR6N8afi/KjSHbXHWw==",
       "peer": true,
       "dependencies": {
-        "@octokit/types": "^6.0.3",
-        "deprecation": "^2.0.0",
-        "once": "^1.4.0"
+        "@octokit/types": "^12.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
       }
     },
+    "node_modules/@octokit/core/node_modules/@octokit/types": {
+      "version": "12.6.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
+      "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/openapi-types": "^20.0.0"
+      }
+    },
+    "node_modules/@octokit/core/node_modules/before-after-hook": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
+      "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
+      "peer": true
+    },
     "node_modules/@octokit/core/node_modules/universal-user-agent": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
-      "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
+      "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==",
       "peer": true
     },
     "node_modules/@octokit/endpoint": {
@@ -70,31 +226,89 @@
       }
     },
     "node_modules/@octokit/endpoint/node_modules/universal-user-agent": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
-      "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
+      "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="
     },
     "node_modules/@octokit/graphql": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
-      "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.0.1.tgz",
+      "integrity": "sha512-lLDb6LhC1gBj2CxEDa5Xk10+H/boonhs+3Mi6jpRyetskDKNHe6crMeKmUE2efoLofMP8ruannLlCUgpTFmVzQ==",
       "peer": true,
       "dependencies": {
-        "@octokit/request": "^5.6.0",
-        "@octokit/types": "^6.0.3",
-        "universal-user-agent": "^6.0.0"
+        "@octokit/request": "^9.0.0",
+        "@octokit/types": "^12.0.0",
+        "universal-user-agent": "^7.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.0.0.tgz",
+      "integrity": "sha512-emBcNDxBdC1y3+knJonS5zhUB/CG6TihubxM2U1/pG/Z1y3a4oV0Gzz3lmkCvWWQI6h3tqBAX9MgCBFp+M68Jw==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/types": "^12.0.0",
+        "universal-user-agent": "^7.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": {
+      "version": "20.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
+      "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
+      "peer": true
+    },
+    "node_modules/@octokit/graphql/node_modules/@octokit/request": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.0.1.tgz",
+      "integrity": "sha512-kL+cAcbSl3dctYLuJmLfx6Iku2MXXy0jszhaEIjQNaCp4zjHXrhVAHeuaRdNvJjW9qjl3u1MJ72+OuBP0YW/pg==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/endpoint": "^10.0.0",
+        "@octokit/request-error": "^6.0.1",
+        "@octokit/types": "^12.0.0",
+        "universal-user-agent": "^7.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/graphql/node_modules/@octokit/request-error": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.0.2.tgz",
+      "integrity": "sha512-WtRVpoHcNXs84+s9s/wqfHaxM68NGMg8Av7h59B50OVO0PwwMx+2GgQ/OliUd0iQBSNWgR6N8afi/KjSHbXHWw==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/types": "^12.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/@octokit/graphql/node_modules/@octokit/types": {
+      "version": "12.6.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
+      "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
+      "peer": true,
+      "dependencies": {
+        "@octokit/openapi-types": "^20.0.0"
       }
     },
     "node_modules/@octokit/graphql/node_modules/universal-user-agent": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
-      "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
+      "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==",
       "peer": true
     },
     "node_modules/@octokit/openapi-types": {
-      "version": "11.2.0",
-      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz",
-      "integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA=="
+      "version": "12.11.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz",
+      "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="
     },
     "node_modules/@octokit/plugin-paginate-rest": {
       "version": "1.1.2",
@@ -179,9 +393,9 @@
       }
     },
     "node_modules/@octokit/request/node_modules/universal-user-agent": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
-      "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
+      "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="
     },
     "node_modules/@octokit/rest": {
       "version": "16.43.2",
@@ -207,11 +421,25 @@
       }
     },
     "node_modules/@octokit/types": {
-      "version": "6.34.0",
-      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz",
-      "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==",
+      "version": "6.41.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
+      "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==",
       "dependencies": {
-        "@octokit/openapi-types": "^11.2.0"
+        "@octokit/openapi-types": "^12.11.0"
+      }
+    },
+    "node_modules/@one-ini/wasm": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
+      "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
       }
     },
     "node_modules/@types/expect": {
@@ -220,23 +448,29 @@
       "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg=="
     },
     "node_modules/@types/node": {
-      "version": "14.18.21",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.21.tgz",
-      "integrity": "sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q=="
+      "version": "20.11.24",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
+      "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
     },
     "node_modules/@types/vinyl": {
-      "version": "2.0.6",
-      "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.6.tgz",
-      "integrity": "sha512-ayJ0iOCDNHnKpKTgBG6Q6JOnHTj9zFta+3j2b8Ejza0e4cvRyMn0ZoLEmbPrTHe5YYRlDYPvPWVdV4cTaRyH7g==",
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.11.tgz",
+      "integrity": "sha512-vPXzCLmRp74e9LsP8oltnWKTH+jBwt86WgRUb4Pc9Lf3pkMVGyvIo2gm9bODeGfCay2DBB/hAWDuvf07JcK4rw==",
       "dependencies": {
         "@types/expect": "^1.20.4",
         "@types/node": "*"
       }
     },
     "node_modules/abbrev": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
-      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
+      "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
     },
     "node_modules/accord": {
       "version": "0.29.0",
@@ -590,9 +824,15 @@
       }
     },
     "node_modules/async-each": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
-      "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ=="
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz",
+      "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ]
     },
     "node_modules/async-settle": {
       "version": "1.0.0",
@@ -703,9 +943,9 @@
       }
     },
     "node_modules/before-after-hook": {
-      "version": "2.2.2",
-      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz",
-      "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ=="
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
+      "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
     },
     "node_modules/better-console": {
       "version": "1.0.1",
@@ -735,6 +975,15 @@
         "url": "https://bevry.me/fund"
       }
     },
+    "node_modules/bindings": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+      "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+      "optional": true,
+      "dependencies": {
+        "file-uri-to-path": "1.0.0"
+      }
+    },
     "node_modules/brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -765,9 +1014,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.20.4",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.4.tgz",
-      "integrity": "sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==",
+      "version": "4.23.0",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
+      "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -776,14 +1025,17 @@
         {
           "type": "tidelift",
           "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
         }
       ],
       "dependencies": {
-        "caniuse-lite": "^1.0.30001349",
-        "electron-to-chromium": "^1.4.147",
-        "escalade": "^3.1.1",
-        "node-releases": "^2.0.5",
-        "picocolors": "^1.0.0"
+        "caniuse-lite": "^1.0.30001587",
+        "electron-to-chromium": "^1.4.668",
+        "node-releases": "^2.0.14",
+        "update-browserslist-db": "^1.0.13"
       },
       "bin": {
         "browserslist": "cli.js"
@@ -792,22 +1044,20 @@
         "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
       }
     },
-    "node_modules/browserslist/node_modules/picocolors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
-    },
     "node_modules/btoa-lite": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
       "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA=="
     },
     "node_modules/buffer-equal": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz",
-      "integrity": "sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz",
+      "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==",
       "engines": {
-        "node": ">=0.4.0"
+        "node": ">=0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/buffer-from": {
@@ -835,12 +1085,18 @@
       }
     },
     "node_modules/call-bind": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
-      "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+      "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
       "dependencies": {
-        "function-bind": "^1.1.1",
-        "get-intrinsic": "^1.0.2"
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -855,9 +1111,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001352",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001352.tgz",
-      "integrity": "sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA==",
+      "version": "1.0.30001591",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
+      "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -866,6 +1122,10 @@
         {
           "type": "tidelift",
           "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
         }
       ]
     },
@@ -948,61 +1208,16 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/class-utils/node_modules/is-accessor-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
-      "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/class-utils/node_modules/is-data-descriptor": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
-      "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/class-utils/node_modules/is-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
-      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+      "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
       "dependencies": {
-        "is-accessor-descriptor": "^0.1.6",
-        "is-data-descriptor": "^0.1.4",
-        "kind-of": "^5.0.0"
+        "is-accessor-descriptor": "^1.0.1",
+        "is-data-descriptor": "^1.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/clean-css": {
@@ -1171,14 +1386,20 @@
       }
     },
     "node_modules/commander": {
-      "version": "2.20.3",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
-      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+      "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+      "engines": {
+        "node": ">=14"
+      }
     },
     "node_modules/component-emitter": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
-      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
+      "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
     },
     "node_modules/concat-map": {
       "version": "0.0.1",
@@ -1217,12 +1438,9 @@
       }
     },
     "node_modules/convert-source-map": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz",
-      "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==",
-      "dependencies": {
-        "safe-buffer": "~5.1.1"
-      }
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+      "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
     },
     "node_modules/copy-anything": {
       "version": "2.0.6",
@@ -1284,12 +1502,15 @@
       }
     },
     "node_modules/d": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
-      "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
+      "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
       "dependencies": {
-        "es5-ext": "^0.10.50",
-        "type": "^1.0.1"
+        "es5-ext": "^0.10.64",
+        "type": "^2.7.2"
+      },
+      "engines": {
+        "node": ">=0.12"
       }
     },
     "node_modules/dateformat": {
@@ -1317,9 +1538,9 @@
       }
     },
     "node_modules/decode-uri-component": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
-      "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
+      "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
       "engines": {
         "node": ">=0.10"
       }
@@ -1336,9 +1557,9 @@
       }
     },
     "node_modules/deepmerge": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
-      "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -1362,11 +1583,28 @@
         "node": ">= 0.10"
       }
     },
-    "node_modules/define-properties": {
+    "node_modules/define-data-property": {
       "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
-      "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
       "dependencies": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/define-properties": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+      "dependencies": {
+        "define-data-property": "^1.0.1",
         "has-property-descriptors": "^1.0.0",
         "object-keys": "^1.1.1"
       },
@@ -1494,24 +1732,73 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
+    },
     "node_modules/editorconfig": {
-      "version": "0.15.3",
-      "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
-      "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
+      "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
       "dependencies": {
-        "commander": "^2.19.0",
-        "lru-cache": "^4.1.5",
-        "semver": "^5.6.0",
-        "sigmund": "^1.0.1"
+        "@one-ini/wasm": "0.1.1",
+        "commander": "^10.0.0",
+        "minimatch": "9.0.1",
+        "semver": "^7.5.3"
       },
       "bin": {
         "editorconfig": "bin/editorconfig"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/editorconfig/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/editorconfig/node_modules/minimatch": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
+      "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/editorconfig/node_modules/semver": {
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.151",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.151.tgz",
-      "integrity": "sha512-XaG2LpZi9fdiWYOqJh0dJy4SlVywCvpgYXhzOlZTp4JqSKqxn5URqOjbm9OMYB3aInA2GuHQiem1QUOc1yT0Pw=="
+      "version": "1.4.690",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz",
+      "integrity": "sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA=="
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
     },
     "node_modules/end-of-stream": {
       "version": "1.4.4",
@@ -1521,6 +1808,18 @@
         "once": "^1.4.0"
       }
     },
+    "node_modules/errno": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
+      "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
+      "optional": true,
+      "dependencies": {
+        "prr": "~1.0.1"
+      },
+      "bin": {
+        "errno": "cli.js"
+      }
+    },
     "node_modules/error-ex": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -1529,14 +1828,34 @@
         "is-arrayish": "^0.2.1"
       }
     },
+    "node_modules/es-define-property": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+      "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+      "dependencies": {
+        "get-intrinsic": "^1.2.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/es5-ext": {
-      "version": "0.10.61",
-      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.61.tgz",
-      "integrity": "sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA==",
+      "version": "0.10.64",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+      "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
       "hasInstallScript": true,
       "dependencies": {
         "es6-iterator": "^2.0.3",
         "es6-symbol": "^3.1.3",
+        "esniff": "^2.0.1",
         "next-tick": "^1.1.0"
       },
       "engines": {
@@ -1554,12 +1873,15 @@
       }
     },
     "node_modules/es6-symbol": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
-      "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
+      "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
       "dependencies": {
-        "d": "^1.0.1",
-        "ext": "^1.1.2"
+        "d": "^1.0.2",
+        "ext": "^1.7.0"
+      },
+      "engines": {
+        "node": ">=0.12"
       }
     },
     "node_modules/es6-weak-map": {
@@ -1574,9 +1896,9 @@
       }
     },
     "node_modules/escalade": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
       "engines": {
         "node": ">=6"
       }
@@ -1589,6 +1911,29 @@
         "node": ">=0.8.0"
       }
     },
+    "node_modules/esniff": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+      "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+      "dependencies": {
+        "d": "^1.0.1",
+        "es5-ext": "^0.10.62",
+        "event-emitter": "^0.3.5",
+        "type": "^2.7.2"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+      "dependencies": {
+        "d": "1",
+        "es5-ext": "~0.10.14"
+      }
+    },
     "node_modules/execa": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
@@ -1634,61 +1979,16 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/expand-brackets/node_modules/is-accessor-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
-      "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/expand-brackets/node_modules/is-data-descriptor": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
-      "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/expand-brackets/node_modules/is-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
-      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+      "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
       "dependencies": {
-        "is-accessor-descriptor": "^0.1.6",
-        "is-data-descriptor": "^0.1.4",
-        "kind-of": "^5.0.0"
+        "is-accessor-descriptor": "^1.0.1",
+        "is-data-descriptor": "^1.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/expand-tilde": {
@@ -1703,18 +2003,13 @@
       }
     },
     "node_modules/ext": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz",
-      "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==",
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+      "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
       "dependencies": {
-        "type": "^2.5.0"
+        "type": "^2.7.2"
       }
     },
-    "node_modules/ext/node_modules/type": {
-      "version": "2.6.0",
-      "resolved": "https://registry.npmjs.org/type/-/type-2.6.0.tgz",
-      "integrity": "sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ=="
-    },
     "node_modules/extend": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -1803,6 +2098,12 @@
         "node": ">=4"
       }
     },
+    "node_modules/file-uri-to-path": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+      "optional": true
+    },
     "node_modules/fill-range": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@@ -1968,6 +2269,86 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/foreground-child": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
+      "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
+      "dependencies": {
+        "cross-spawn": "^7.0.0",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/foreground-child/node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/foreground-child/node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/foreground-child/node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/foreground-child/node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/foreground-child/node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/foreground-child/node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
     "node_modules/fork-stream": {
       "version": "0.0.4",
       "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz",
@@ -2010,10 +2391,31 @@
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
       "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
     },
+    "node_modules/fsevents": {
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
+      "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
+      "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2",
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "dependencies": {
+        "bindings": "^1.5.0",
+        "nan": "^2.12.1"
+      },
+      "engines": {
+        "node": ">= 4.0"
+      }
+    },
     "node_modules/function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
     "node_modules/get-caller-file": {
       "version": "1.0.3",
@@ -2033,13 +2435,18 @@
       }
     },
     "node_modules/get-intrinsic": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz",
-      "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==",
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+      "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
       "dependencies": {
-        "function-bind": "^1.1.1",
-        "has": "^1.0.3",
-        "has-symbols": "^1.0.3"
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "has-proto": "^1.0.1",
+        "has-symbols": "^1.0.3",
+        "hasown": "^2.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -2216,10 +2623,21 @@
         "node": ">= 0.10"
       }
     },
+    "node_modules/gopd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+      "dependencies": {
+        "get-intrinsic": "^1.1.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/graceful-fs": {
-      "version": "4.2.10",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
-      "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
     },
     "node_modules/growly": {
       "version": "1.3.0",
@@ -2889,21 +3307,32 @@
       }
     },
     "node_modules/gulp-json-editor": {
-      "version": "2.5.6",
-      "resolved": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.5.6.tgz",
-      "integrity": "sha512-66Xr6Q6m4mUNd0OOHflMB/RHgFNnLjlHgizOzUcx9CyMRymVZEM+/SpZcCDlvThBdXtQwXpdvtSepxVY/V6nQA==",
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.6.0.tgz",
+      "integrity": "sha512-Ni0ZUpNrhesHiTlHQth/Nv1rXCn0LUicEvzA5XuGy186C4PVeNoRjfuAIQrbmt3scKv8dgGbCs0hd77ScTw7hA==",
       "dependencies": {
-        "deepmerge": "^4.2.2",
-        "detect-indent": "^6.0.0",
-        "js-beautify": "^1.13.13",
-        "plugin-error": "^1.0.1",
+        "deepmerge": "^4.3.1",
+        "detect-indent": "^6.1.0",
+        "js-beautify": "^1.14.11",
+        "plugin-error": "^2.0.1",
         "through2": "^4.0.2"
       }
     },
+    "node_modules/gulp-json-editor/node_modules/plugin-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-2.0.1.tgz",
+      "integrity": "sha512-zMakqvIDyY40xHOvzXka0kUvf40nYIuwRE8dWhti2WtjQZ31xAgBZBhxsK7vK3QbRXS1Xms/LO7B5cuAsfB2Gg==",
+      "dependencies": {
+        "ansi-colors": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
     "node_modules/gulp-json-editor/node_modules/readable-stream": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
-      "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
       "dependencies": {
         "inherits": "^2.0.3",
         "string_decoder": "^1.1.1",
@@ -3228,11 +3657,11 @@
       }
     },
     "node_modules/gulp-replace": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.3.tgz",
-      "integrity": "sha512-HcPHpWY4XdF8zxYkDODHnG2+7a3nD/Y8Mfu3aBgMiCFDW3X2GiOKXllsAmILcxe3KZT2BXoN18WrpEFm48KfLQ==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.4.tgz",
+      "integrity": "sha512-SVSF7ikuWKhpAW4l4wapAqPPSToJoiNKsbDoUnRrSgwZHH7lH8pbPeQj1aOVYQrbZKhfSVBxVW+Py7vtulRktw==",
       "dependencies": {
-        "@types/node": "^14.14.41",
+        "@types/node": "*",
         "@types/vinyl": "^2.0.4",
         "istextorbinary": "^3.0.0",
         "replacestream": "^4.0.3",
@@ -3340,9 +3769,9 @@
       }
     },
     "node_modules/gulp-uglify/node_modules/uglify-js": {
-      "version": "3.16.0",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.0.tgz",
-      "integrity": "sha512-FEikl6bR30n0T3amyBh3LoiBdqHRy/f4H80+My34HOesOKyHfOsxAPAxOoqC0JUnC1amnO0IwkYC3sko51caSw==",
+      "version": "3.17.4",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
+      "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
       "bin": {
         "uglifyjs": "bin/uglifyjs"
       },
@@ -3445,7 +3874,7 @@
     "node_modules/gulp-util/node_modules/vinyl": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz",
-      "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=",
+      "integrity": "sha512-P5zdf3WB9uzr7IFoVQ2wZTmUwHL8cMZWJGzLBNCHNZ3NB6HTMsYABtt7z8tAGIINLXyAob9B9a1yzVGMFOYKEA==",
       "dependencies": {
         "clone": "^1.0.0",
         "clone-stats": "^0.0.1",
@@ -3466,17 +3895,6 @@
         "node": ">= 0.10"
       }
     },
-    "node_modules/has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dependencies": {
-        "function-bind": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 0.4.0"
-      }
-    },
     "node_modules/has-ansi": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
@@ -3508,11 +3926,22 @@
       }
     },
     "node_modules/has-property-descriptors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
-      "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
       "dependencies": {
-        "get-intrinsic": "^1.1.1"
+        "es-define-property": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-proto": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+      "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -3565,6 +3994,17 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/hasown": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
+      "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/homedir-polyfill": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -3592,6 +4032,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/image-size": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+      "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
+      "optional": true,
+      "bin": {
+        "image-size": "bin/image-size.js"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/import-regex": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/import-regex/-/import-regex-1.1.0.tgz",
@@ -3754,22 +4206,14 @@
       }
     },
     "node_modules/is-accessor-descriptor": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
-      "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz",
+      "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==",
       "dependencies": {
-        "kind-of": "^6.0.0"
+        "hasown": "^2.0.0"
       },
       "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-accessor-descriptor/node_modules/kind-of": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
-      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
-      "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.10"
       }
     },
     "node_modules/is-arrayish": {
@@ -3794,54 +4238,37 @@
       "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
     },
     "node_modules/is-core-module": {
-      "version": "2.9.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz",
-      "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==",
+      "version": "2.13.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
+      "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
       "dependencies": {
-        "has": "^1.0.3"
+        "hasown": "^2.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/is-data-descriptor": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
-      "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz",
+      "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==",
       "dependencies": {
-        "kind-of": "^6.0.0"
+        "hasown": "^2.0.0"
       },
       "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-data-descriptor/node_modules/kind-of": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
-      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
-      "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/is-descriptor": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
-      "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz",
+      "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==",
       "dependencies": {
-        "is-accessor-descriptor": "^1.0.0",
-        "is-data-descriptor": "^1.0.0",
-        "kind-of": "^6.0.2"
+        "is-accessor-descriptor": "^1.0.1",
+        "is-data-descriptor": "^1.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-descriptor/node_modules/kind-of": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
-      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
-      "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/is-extendable": {
@@ -4060,20 +4487,38 @@
         "url": "https://bevry.me/fund"
       }
     },
+    "node_modules/jackspeak": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
+      "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
     "node_modules/jquery": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
-      "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
+      "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
     },
     "node_modules/js-beautify": {
-      "version": "1.14.3",
-      "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.3.tgz",
-      "integrity": "sha512-f1ra8PHtOEu/70EBnmiUlV8nJePS58y9qKjl4JHfYWlFH6bo7ogZBz//FAZp7jDuXtYnGYKymZPlrg2I/9Zo4g==",
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
+      "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==",
       "dependencies": {
         "config-chain": "^1.1.13",
-        "editorconfig": "^0.15.3",
-        "glob": "^7.1.3",
-        "nopt": "^5.0.0"
+        "editorconfig": "^1.0.4",
+        "glob": "^10.3.3",
+        "js-cookie": "^3.0.5",
+        "nopt": "^7.2.0"
       },
       "bin": {
         "css-beautify": "js/bin/css-beautify.js",
@@ -4081,7 +4526,58 @@
         "js-beautify": "js/bin/js-beautify.js"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">=14"
+      }
+    },
+    "node_modules/js-beautify/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/js-beautify/node_modules/glob": {
+      "version": "10.3.10",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+      "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^2.3.5",
+        "minimatch": "^9.0.1",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+        "path-scurry": "^1.10.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/js-beautify/node_modules/minimatch": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/js-cookie": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+      "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+      "engines": {
+        "node": ">=14"
       }
     },
     "node_modules/json-stable-stringify-without-jsonify": {
@@ -4449,18 +4945,20 @@
       }
     },
     "node_modules/lru-cache": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
-      "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
       "dependencies": {
-        "pseudomap": "^1.0.2",
-        "yallist": "^2.1.2"
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
       }
     },
     "node_modules/macos-release": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz",
-      "integrity": "sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==",
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz",
+      "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==",
       "engines": {
         "node": ">=6"
       },
@@ -4468,6 +4966,28 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/make-dir": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+      "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+      "optional": true,
+      "dependencies": {
+        "pify": "^4.0.1",
+        "semver": "^5.6.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/make-dir/node_modules/pify": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+      "optional": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/make-error": {
       "version": "1.3.6",
       "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -4633,6 +5153,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "optional": true,
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/mimic-fn": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
@@ -4653,9 +5185,20 @@
       }
     },
     "node_modules/minimist": {
-      "version": "1.2.6",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
-      "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
+      "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
     },
     "node_modules/mixin-deep": {
       "version": "1.3.2",
@@ -4728,6 +5271,12 @@
       "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
       "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ=="
     },
+    "node_modules/nan": {
+      "version": "2.18.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
+      "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
+      "optional": true
+    },
     "node_modules/nanomatch": {
       "version": "1.2.13",
       "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -4791,6 +5340,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/native-request": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.1.0.tgz",
+      "integrity": "sha512-uZ5rQaeRn15XmpgE0xoPL8YWqcX90VtCFglYwAgkvKM5e8fog+vePLAhHxuuv/gRkrQxIeh5U3q9sMNUrENqWw==",
+      "optional": true
+    },
     "node_modules/next-tick": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
@@ -4802,9 +5357,9 @@
       "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
     },
     "node_modules/node-fetch": {
-      "version": "2.6.7",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
-      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
       "dependencies": {
         "whatwg-url": "^5.0.0"
       },
@@ -4833,34 +5388,34 @@
       }
     },
     "node_modules/node-releases": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz",
-      "integrity": "sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q=="
+      "version": "2.0.14",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+      "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
     },
     "node_modules/node.extend": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.2.tgz",
-      "integrity": "sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.3.tgz",
+      "integrity": "sha512-xwADg/okH48PvBmRZyoX8i8GJaKuJ1CqlqotlZOhUio8egD1P5trJupHKBzcPjSF9ifK2gPcEICRBnkfPqQXZw==",
       "dependencies": {
-        "has": "^1.0.3",
-        "is": "^3.2.1"
+        "hasown": "^2.0.0",
+        "is": "^3.3.0"
       },
       "engines": {
         "node": ">=0.4.0"
       }
     },
     "node_modules/nopt": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
-      "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz",
+      "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==",
       "dependencies": {
-        "abbrev": "1"
+        "abbrev": "^2.0.0"
       },
       "bin": {
         "nopt": "bin/nopt.js"
       },
       "engines": {
-        "node": ">=6"
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
       }
     },
     "node_modules/normalize-package-data": {
@@ -4957,47 +5512,16 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/object-copy/node_modules/is-accessor-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
-      "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/object-copy/node_modules/is-data-descriptor": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
-      "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/object-copy/node_modules/is-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
-      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+      "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
       "dependencies": {
-        "is-accessor-descriptor": "^0.1.6",
-        "is-data-descriptor": "^0.1.4",
-        "kind-of": "^5.0.0"
+        "is-accessor-descriptor": "^1.0.1",
+        "is-data-descriptor": "^1.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
-      "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
-      "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/object-copy/node_modules/kind-of": {
@@ -5031,13 +5555,13 @@
       }
     },
     "node_modules/object.assign": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
-      "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
+      "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
       "dependencies": {
-        "call-bind": "^1.0.0",
-        "define-properties": "^1.1.3",
-        "has-symbols": "^1.0.1",
+        "call-bind": "^1.0.5",
+        "define-properties": "^1.2.1",
+        "has-symbols": "^1.0.3",
         "object-keys": "^1.1.1"
       },
       "engines": {
@@ -5303,6 +5827,29 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/path-scurry": {
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
+      "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
+      "dependencies": {
+        "lru-cache": "^9.1.1 || ^10.0.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
+      "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
+      "engines": {
+        "node": "14 || >=16.14"
+      }
+    },
     "node_modules/path-type": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
@@ -5462,10 +6009,11 @@
       "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
       "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="
     },
-    "node_modules/pseudomap": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
-      "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="
+    "node_modules/prr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+      "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
+      "optional": true
     },
     "node_modules/pump": {
       "version": "2.0.1",
@@ -5512,9 +6060,9 @@
       }
     },
     "node_modules/readable-stream": {
-      "version": "2.3.7",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
-      "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
       "dependencies": {
         "core-util-is": "~1.0.0",
         "inherits": "~2.0.3",
@@ -5525,6 +6073,11 @@
         "util-deprecate": "~1.0.1"
       }
     },
+    "node_modules/readable-stream/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
     "node_modules/readdirp": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
@@ -5708,11 +6261,11 @@
       "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug=="
     },
     "node_modules/resolve": {
-      "version": "1.22.0",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
-      "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
+      "version": "1.22.8",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
       "dependencies": {
-        "is-core-module": "^2.8.1",
+        "is-core-module": "^2.13.0",
         "path-parse": "^1.0.7",
         "supports-preserve-symlinks-flag": "^1.0.0"
       },
@@ -5963,9 +6516,23 @@
       }
     },
     "node_modules/safe-buffer": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
     },
     "node_modules/safe-regex": {
       "version": "1.1.0",
@@ -5981,9 +6548,9 @@
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
     "node_modules/semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
       "bin": {
         "semver": "bin/semver"
       }
@@ -6004,6 +6571,22 @@
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
       "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
     },
+    "node_modules/set-function-length": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
+      "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
+      "dependencies": {
+        "define-data-property": "^1.1.2",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.3",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/set-value": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
@@ -6053,11 +6636,6 @@
       "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
       "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww=="
     },
-    "node_modules/sigmund": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
-      "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g=="
-    },
     "node_modules/signal-exit": {
       "version": "3.0.7",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -6138,61 +6716,16 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/snapdragon/node_modules/is-accessor-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
-      "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/snapdragon/node_modules/is-data-descriptor": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
-      "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/snapdragon/node_modules/is-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
-      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+      "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
       "dependencies": {
-        "is-accessor-descriptor": "^0.1.6",
-        "is-data-descriptor": "^0.1.4",
-        "kind-of": "^5.0.0"
+        "is-accessor-descriptor": "^1.0.1",
+        "is-data-descriptor": "^1.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/snapdragon/node_modules/source-map": {
@@ -6239,18 +6772,18 @@
       }
     },
     "node_modules/spdx-correct": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
-      "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
       "dependencies": {
         "spdx-expression-parse": "^3.0.0",
         "spdx-license-ids": "^3.0.0"
       }
     },
     "node_modules/spdx-exceptions": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
-      "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A=="
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+      "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="
     },
     "node_modules/spdx-expression-parse": {
       "version": "3.0.1",
@@ -6262,9 +6795,9 @@
       }
     },
     "node_modules/spdx-license-ids": {
-      "version": "3.0.11",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz",
-      "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g=="
+      "version": "3.0.17",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz",
+      "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg=="
     },
     "node_modules/split-string": {
       "version": "3.1.0",
@@ -6352,61 +6885,16 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/static-extend/node_modules/is-accessor-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
-      "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/static-extend/node_modules/is-data-descriptor": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
-      "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
-      "dependencies": {
-        "kind-of": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
-      "dependencies": {
-        "is-buffer": "^1.1.5"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/static-extend/node_modules/is-descriptor": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
-      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+      "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
       "dependencies": {
-        "is-accessor-descriptor": "^0.1.6",
-        "is-data-descriptor": "^0.1.4",
-        "kind-of": "^5.0.0"
+        "is-accessor-descriptor": "^1.0.1",
+        "is-data-descriptor": "^1.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 0.4"
       }
     },
     "node_modules/stream-exhaust": {
@@ -6415,9 +6903,9 @@
       "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw=="
     },
     "node_modules/stream-shift": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
-      "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
+      "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="
     },
     "node_modules/string_decoder": {
       "version": "1.1.1",
@@ -6427,6 +6915,11 @@
         "safe-buffer": "~5.1.0"
       }
     },
+    "node_modules/string_decoder/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
     "node_modules/string-width": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
@@ -6439,6 +6932,47 @@
         "node": ">=4"
       }
     },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/string-width/node_modules/ansi-regex": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
@@ -6482,6 +7016,26 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/strip-bom": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
@@ -6667,7 +7221,7 @@
     "node_modules/to-absolute-glob": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz",
-      "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=",
+      "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==",
       "dependencies": {
         "is-absolute": "^1.0.0",
         "is-negated-glob": "^1.0.0"
@@ -6679,7 +7233,7 @@
     "node_modules/to-object-path": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
-      "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+      "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==",
       "dependencies": {
         "kind-of": "^3.0.2"
       },
@@ -6715,7 +7269,7 @@
     "node_modules/to-regex-range": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
-      "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+      "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==",
       "dependencies": {
         "is-number": "^3.0.0",
         "repeat-string": "^1.6.1"
@@ -6761,7 +7315,7 @@
     "node_modules/to-through": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz",
-      "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=",
+      "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==",
       "dependencies": {
         "through2": "^2.0.3"
       },
@@ -6781,7 +7335,7 @@
     "node_modules/tr46": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
     },
     "node_modules/tslib": {
       "version": "1.14.1",
@@ -6789,19 +7343,19 @@
       "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
     },
     "node_modules/type": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
-      "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
+      "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
     },
     "node_modules/typedarray": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
-      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+      "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
     },
     "node_modules/uglify-js": {
       "version": "2.8.29",
       "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
-      "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
+      "integrity": "sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==",
       "dependencies": {
         "source-map": "~0.5.1",
         "yargs": "~3.10.0"
@@ -6845,7 +7399,7 @@
     "node_modules/uglify-js/node_modules/yargs": {
       "version": "3.10.0",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
-      "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
+      "integrity": "sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==",
       "dependencies": {
         "camelcase": "^1.0.2",
         "cliui": "^2.1.0",
@@ -6853,10 +7407,16 @@
         "window-size": "0.1.0"
       }
     },
+    "node_modules/uglify-to-browserify": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz",
+      "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==",
+      "optional": true
+    },
     "node_modules/unc-path-regex": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
-      "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=",
+      "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -6884,11 +7444,16 @@
     "node_modules/undertaker-registry": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz",
-      "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=",
+      "integrity": "sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw==",
       "engines": {
         "node": ">= 0.10"
       }
     },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+    },
     "node_modules/union-value": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -6923,7 +7488,7 @@
     "node_modules/unset-value": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
-      "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+      "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==",
       "dependencies": {
         "has-value": "^0.3.1",
         "isobject": "^3.0.0"
@@ -6973,16 +7538,50 @@
         "yarn": "*"
       }
     },
+    "node_modules/update-browserslist-db": {
+      "version": "1.0.13",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
+      "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "escalade": "^3.1.1",
+        "picocolors": "^1.0.0"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/update-browserslist-db/node_modules/picocolors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+    },
     "node_modules/urix": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
-      "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+      "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==",
       "deprecated": "Please see https://github.com/lydell/urix#deprecated"
     },
     "node_modules/url-regex": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/url-regex/-/url-regex-3.2.0.tgz",
-      "integrity": "sha1-260eDJ4p4QXdCx8J9oYvf9tIJyQ=",
+      "integrity": "sha512-dQ9cJzMou5OKr6ZzfvwJkCq3rC72PNXhqz0v3EIhF4a3Np+ujr100AhUx2cKx5ei3iymoJpJrPB3sVSEMdqAeg==",
       "dependencies": {
         "ip-regex": "^1.0.1"
       },
@@ -7001,7 +7600,7 @@
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
     },
     "node_modules/v8flags": {
       "version": "3.2.0",
@@ -7026,7 +7625,7 @@
     "node_modules/value-or-function": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz",
-      "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=",
+      "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==",
       "engines": {
         "node": ">= 0.10"
       }
@@ -7086,7 +7685,7 @@
     "node_modules/vinyl-sourcemap": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz",
-      "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=",
+      "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==",
       "dependencies": {
         "append-buffer": "^1.0.2",
         "convert-source-map": "^1.5.0",
@@ -7114,7 +7713,7 @@
     "node_modules/vinyl-sourcemaps-apply": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz",
-      "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=",
+      "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==",
       "dependencies": {
         "source-map": "^0.5.1"
       }
@@ -7130,12 +7729,12 @@
     "node_modules/webidl-conversions": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
     },
     "node_modules/whatwg-url": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
       "dependencies": {
         "tr46": "~0.0.3",
         "webidl-conversions": "^3.0.0"
@@ -7144,7 +7743,7 @@
     "node_modules/when": {
       "version": "3.7.8",
       "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz",
-      "integrity": "sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I="
+      "integrity": "sha512-5cZ7mecD3eYcMiCH4wtRPA5iFJZ50BJYDfckI5RRpQiktMiYTcn0ccLTZOvcbBume+1304fQztxeNzNS9Gvrnw=="
     },
     "node_modules/which": {
       "version": "1.3.1",
@@ -7160,12 +7759,12 @@
     "node_modules/which-module": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
-      "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8="
+      "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ=="
     },
     "node_modules/window-size": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
-      "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=",
+      "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==",
       "engines": {
         "node": ">= 0.8.0"
       }
@@ -7187,7 +7786,7 @@
     "node_modules/wordwrap": {
       "version": "0.0.2",
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
-      "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
+      "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==",
       "engines": {
         "node": ">=0.4.0"
       }
@@ -7195,7 +7794,7 @@
     "node_modules/wrap-ansi": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
-      "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+      "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==",
       "dependencies": {
         "string-width": "^1.0.1",
         "strip-ansi": "^3.0.1"
@@ -7204,6 +7803,93 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
@@ -7231,12 +7917,12 @@
     "node_modules/wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
     },
     "node_modules/wrench-sui": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/wrench-sui/-/wrench-sui-0.0.3.tgz",
-      "integrity": "sha1-1hoSAwwf2NZxs90VqmyeD83E4sg=",
+      "integrity": "sha512-Y6qzMpcMG9akKnIdUsKzEF/Ht0KQJBP8ETkZj3FcGe93NC71e940WZUP1y+j+hc8Ecx9TyX0GvAWC4yymA88yA==",
       "engines": {
         "node": ">=0.1.97"
       }
@@ -7255,9 +7941,9 @@
       "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ=="
     },
     "node_modules/yallist": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
-      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
     },
     "node_modules/yamljs": {
       "version": "0.3.0",
@@ -7293,9 +7979,9 @@
       }
     },
     "node_modules/yargs-parser": {
-      "version": "21.0.1",
-      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
-      "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==",
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
       "engines": {
         "node": ">=12"
       }
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
index 43d0b412b3..5db57bc8d4 100644
--- a/web_src/fomantic/semantic.json
+++ b/web_src/fomantic/semantic.json
@@ -23,23 +23,12 @@
   "components": [
     "api",
     "button",
-    "checkbox",
-    "container",
     "dimmer",
     "dropdown",
     "form",
-    "grid",
-    "header",
-    "input",
-    "label",
-    "list",
     "menu",
-    "message",
     "modal",
     "search",
-    "segment",
-    "site",
-    "tab",
-    "table"
+    "tab"
   ]
 }
diff --git a/web_src/js/bootstrap.js b/web_src/js/bootstrap.js
index f8d0c0cac0..3034478190 100644
--- a/web_src/js/bootstrap.js
+++ b/web_src/js/bootstrap.js
@@ -1,20 +1,29 @@
 // DO NOT IMPORT window.config HERE!
-// to make sure the error handler always works, we should never import `window.config`, because some user's custom template breaks it.
+// to make sure the error handler always works, we should never import `window.config`, because
+// some user's custom template breaks it.
 
 // This sets up the URL prefix used in webpack's chunk loading.
 // This file must be imported before any lazy-loading is being attempted.
 __webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`;
 
+const filteredErrors = new Set([
+  'getModifierState is not a function', // https://github.com/microsoft/monaco-editor/issues/4325
+]);
+
 export function showGlobalErrorMessage(msg) {
   const pageContent = document.querySelector('.page-content');
   if (!pageContent) return;
 
+  for (const filteredError of filteredErrors) {
+    if (msg.includes(filteredError)) return;
+  }
+
   // compact the message to a data attribute to avoid too many duplicated messages
   const msgCompact = msg.replace(/\W/g, '').trim();
   let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
   if (!msgDiv) {
     const el = document.createElement('div');
-    el.innerHTML = `<div class="ui container negative message center aligned js-global-error" style="white-space: pre-line;"></div>`;
+    el.innerHTML = `<div class="ui container negative message center aligned js-global-error tw-mt-[15px] tw-whitespace-pre-line"></div>`;
     msgDiv = el.childNodes[0];
   }
   // merge duplicated messages into "the message (count)" format
@@ -26,20 +35,42 @@ export function showGlobalErrorMessage(msg) {
 }
 
 /**
- * @param {ErrorEvent} e
+ * @param {ErrorEvent|PromiseRejectionEvent} event - Event
+ * @param {string} event.message - Only present on ErrorEvent
+ * @param {string} event.error - Only present on ErrorEvent
+ * @param {string} event.type - Only present on ErrorEvent
+ * @param {string} event.filename - Only present on ErrorEvent
+ * @param {number} event.lineno - Only present on ErrorEvent
+ * @param {number} event.colno - Only present on ErrorEvent
+ * @param {string} event.reason - Only present on PromiseRejectionEvent
+ * @param {number} event.promise - Only present on PromiseRejectionEvent
  */
-function processWindowErrorEvent(e) {
-  if (e.type === 'unhandledrejection') {
-    showGlobalErrorMessage(`JavaScript promise rejection: ${e.reason}. Open browser console to see more details.`);
-    return;
-  }
-  if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) {
-    // At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240
-    // If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0.
-    return; // ignore such nonsense error event
+function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}) {
+  const err = error ?? reason;
+  const assetBaseUrl = String(new URL(__webpack_public_path__, window.location.origin));
+  const {runModeIsProd} = window.config ?? {};
+
+  // `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likly a
+  // non-critical event from the browser. We log them but don't show them to users. Examples:
+  // - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors
+  // - https://github.com/mozilla-mobile/firefox-ios/issues/10817
+  // - https://github.com/go-gitea/gitea/issues/20240
+  if (!err) {
+    if (message) console.error(new Error(message));
+    if (runModeIsProd) return;
   }
 
-  showGlobalErrorMessage(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`);
+  // If the error stack trace does not include the base URL of our script assets, it likely came
+  // from a browser extension or inline script. Do not show such errors in production.
+  if (err instanceof Error && !err.stack?.includes(assetBaseUrl) && runModeIsProd) {
+    return;
+  }
+
+  let msg = err?.message ?? message;
+  if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`;
+  const dot = msg.endsWith('.') ? '' : '.';
+  const renderedType = type === 'unhandledrejection' ? 'promise rejection' : type;
+  showGlobalErrorMessage(`JavaScript ${renderedType}: ${msg}${dot} Open browser console to see more details.`);
 }
 
 function initGlobalErrorHandler() {
@@ -50,13 +81,14 @@ function initGlobalErrorHandler() {
   if (!window.config) {
     showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`);
   }
-  // we added an event handler for window error at the very beginning of <script> of page head
-  // the handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before this init
-  // then in this init, we can collect all error events and show them
+  // we added an event handler for window error at the very beginning of <script> of page head the
+  // handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before
+  // this init then in this init, we can collect all error events and show them.
   for (const e of window._globalHandlerErrors || []) {
     processWindowErrorEvent(e);
   }
-  // then, change _globalHandlerErrors to an object with push method, to process further error events directly
+  // then, change _globalHandlerErrors to an object with push method, to process further error
+  // events directly
   window._globalHandlerErrors = {_inited: true, push: (e) => processWindowErrorEvent(e)};
 }
 
diff --git a/web_src/js/components/.eslintrc.yaml b/web_src/js/components/.eslintrc.yaml
index 0cab470f6b..0d233442bc 100644
--- a/web_src/js/components/.eslintrc.yaml
+++ b/web_src/js/components/.eslintrc.yaml
@@ -7,6 +7,10 @@ extends:
   - plugin:vue/vue3-recommended
   - plugin:vue-scoped-css/vue3-recommended
 
+parserOptions:
+  sourceType: module
+  ecmaVersion: latest
+
 env:
   browser: true
 
diff --git a/web_src/js/components/ActionRunStatus.vue b/web_src/js/components/ActionRunStatus.vue
index 51a7745431..7ada543fea 100644
--- a/web_src/js/components/ActionRunStatus.vue
+++ b/web_src/js/components/ActionRunStatus.vue
@@ -10,25 +10,25 @@ export default {
   props: {
     status: {
       type: String,
-      required: true
+      required: true,
     },
     size: {
       type: Number,
-      default: 16
+      default: 16,
     },
     className: {
       type: String,
-      default: ''
+      default: '',
     },
     localeStatus: {
       type: String,
-      default: ''
-    }
+      default: '',
+    },
   },
 };
 </script>
 <template>
-  <span class="gt-df gt-ac" :data-tooltip-content="localeStatus" v-if="status">
+  <span class="tw-flex tw-items-center" :data-tooltip-content="localeStatus" v-if="status">
     <SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/>
     <SvgIcon name="octicon-skip" class="text grey" :size="size" :class-name="className" v-else-if="status === 'skipped'"/>
     <SvgIcon name="octicon-clock" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'waiting'"/>
diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue
index 96a6e68012..9592a0df3c 100644
--- a/web_src/js/components/ActivityHeatmap.vue
+++ b/web_src/js/components/ActivityHeatmap.vue
@@ -11,7 +11,7 @@ export default {
     locale: {
       type: Object,
       default: () => {},
-    }
+    },
   },
   data: () => ({
     colorRange: [
@@ -49,7 +49,7 @@ export default {
 
       const newSearch = params.toString();
       window.location.search = newSearch.length ? `?${newSearch}` : '';
-    }
+    },
   },
 };
 </script>
diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue
index d9e6da316c..65a6089522 100644
--- a/web_src/js/components/ContextPopup.vue
+++ b/web_src/js/components/ContextPopup.vue
@@ -1,8 +1,7 @@
 <script>
-import $ from 'jquery';
 import {SvgIcon} from '../svg.js';
-import {useLightTextOnBackground} from '../utils/color.js';
-import tinycolor from 'tinycolor2';
+import {contrastColor} from '../utils/color.js';
+import {GET} from '../modules/fetch.js';
 
 const {appSubUrl, i18n} = window.config;
 
@@ -59,17 +58,12 @@ export default {
     },
 
     labels() {
-      return this.issue.labels.map((label) => {
-        let textColor;
-        const {r, g, b} = tinycolor(label.color).toRgb();
-        if (useLightTextOnBackground(r, g, b)) {
-          textColor = '#eeeeee';
-        } else {
-          textColor = '#111111';
-        }
-        return {name: label.name, color: `#${label.color}`, textColor};
-      });
-    }
+      return this.issue.labels.map((label) => ({
+        name: label.name,
+        color: `#${label.color}`,
+        textColor: contrastColor(`#${label.color}`),
+      }));
+    },
   },
   mounted() {
     this.$refs.root.addEventListener('ce-load-context-popup', (e) => {
@@ -80,32 +74,35 @@ export default {
     });
   },
   methods: {
-    load(data) {
+    async load(data) {
       this.loading = true;
       this.i18nErrorMessage = null;
-      $.get(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`).done((issue) => {
-        this.issue = issue;
-      }).fail((jqXHR) => {
-        if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
-          this.i18nErrorMessage = jqXHR.responseJSON.message;
-        } else {
-          this.i18nErrorMessage = i18n.network_error;
+
+      try {
+        const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`);
+        const respJson = await response.json();
+        if (!response.ok) {
+          this.i18nErrorMessage = respJson.message ?? i18n.network_error;
+          return;
         }
-      }).always(() => {
+        this.issue = respJson;
+      } catch {
+        this.i18nErrorMessage = i18n.network_error;
+      } finally {
         this.loading = false;
-      });
-    }
-  }
+      }
+    },
+  },
 };
 </script>
 <template>
   <div ref="root">
-    <div v-if="loading" class="ui active centered inline loader"/>
+    <div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/>
     <div v-if="!loading && issue !== null">
       <p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p>
       <p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p>
       <p>{{ body }}</p>
-      <div>
+      <div class="labels-list">
         <div
           v-for="label in labels"
           :key="label.name"
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 6f742bbea0..2d980a1b18 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -235,7 +235,7 @@ const sfc = {
         if (!this.reposTotalCount) {
           const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
           response = await GET(totalCountSearchURL);
-          this.reposTotalCount = response.headers.get('X-Total-Count');
+          this.reposTotalCount = response.headers.get('X-Total-Count') ?? '?';
         }
 
         response = await GET(searchedURL);
@@ -253,7 +253,7 @@ const sfc = {
             ...webSearchRepo.repository,
             latest_commit_status_state: webSearchRepo.latest_commit_status.State,
             locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status,
-            latest_commit_status_state_link: webSearchRepo.latest_commit_status.TargetURL
+            latest_commit_status_state_link: webSearchRepo.latest_commit_status.TargetURL,
           };
         });
         const count = response.headers.get('X-Total-Count');
@@ -325,7 +325,7 @@ const sfc = {
       if (this.activeIndex === -1 || this.activeIndex > this.repos.length - 1) {
         this.activeIndex = 0;
       }
-    }
+    },
   },
 };
 
@@ -345,19 +345,19 @@ export default sfc; // activate the IDE's Vue plugin
       <a :class="{item: true, active: tab === 'organizations'}" @click="changeTab('organizations')">{{ textOrganization }}</a>
     </div>
     <div v-show="tab === 'repos'" class="ui tab active list dashboard-repos">
-      <h4 class="ui top attached header gt-df gt-ac">
-        <div class="gt-f1 gt-df gt-ac">
+      <h4 class="ui top attached header tw-flex tw-items-center">
+        <div class="tw-flex-1 tw-flex tw-items-center">
           {{ textMyRepos }}
-          <span class="ui grey label gt-ml-3">{{ reposTotalCount }}</span>
+          <span class="ui grey label tw-ml-2">{{ reposTotalCount }}</span>
         </div>
-        <a class="gt-df gt-ac muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo">
+        <a class="tw-flex tw-items-center muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo">
           <svg-icon name="octicon-plus"/>
         </a>
       </h4>
       <div class="ui attached segment repos-search">
-        <div class="ui fluid action left icon input" :class="{loading: isLoading}">
+        <div class="ui small fluid action left icon input">
           <input type="search" spellcheck="false" maxlength="255" @input="changeReposFilter(reposFilter)" v-model="searchQuery" ref="search" @keydown="reposFilterKeyControl" :placeholder="textSearchRepos">
-          <i class="icon"><svg-icon name="octicon-search" :size="16"/></i>
+          <i class="icon loading-icon-3px" :class="{'is-loading': isLoading}"><svg-icon name="octicon-search" :size="16"/></i>
           <div class="ui dropdown icon button" :title="textFilter">
             <svg-icon name="octicon-filter" :size="16"/>
             <div class="menu">
@@ -367,7 +367,7 @@ export default sfc; // activate the IDE's Vue plugin
                       otherwise if the "input" handles click event for intermediate status, it breaks the internal state-->
                   <input type="checkbox" class="hidden" v-bind.prop="checkboxArchivedFilterProps">
                   <label>
-                    <svg-icon name="octicon-archive" :size="16" class-name="gt-mr-2"/>
+                    <svg-icon name="octicon-archive" :size="16" class-name="tw-mr-1"/>
                     {{ textShowArchived }}
                   </label>
                 </div>
@@ -376,7 +376,7 @@ export default sfc; // activate the IDE's Vue plugin
                 <div class="ui checkbox" ref="checkboxPrivateFilter" :title="checkboxPrivateFilterTitle">
                   <input type="checkbox" class="hidden" v-bind.prop="checkboxPrivateFilterProps">
                   <label>
-                    <svg-icon name="octicon-lock" :size="16" class-name="gt-mr-2"/>
+                    <svg-icon name="octicon-lock" :size="16" class-name="tw-mr-1"/>
                     {{ textShowPrivate }}
                   </label>
                 </div>
@@ -384,32 +384,34 @@ export default sfc; // activate the IDE's Vue plugin
             </div>
           </div>
         </div>
-        <div class="ui secondary tiny pointing borderless menu center grid repos-filter">
-          <a class="item" :class="{active: reposFilter === 'all'}" @click="changeReposFilter('all')">
-            {{ textAll }}
-            <div v-show="reposFilter === 'all'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
-          </a>
-          <a class="item" :class="{active: reposFilter === 'sources'}" @click="changeReposFilter('sources')">
-            {{ textSources }}
-            <div v-show="reposFilter === 'sources'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
-          </a>
-          <a class="item" :class="{active: reposFilter === 'forks'}" @click="changeReposFilter('forks')">
-            {{ textForks }}
-            <div v-show="reposFilter === 'forks'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
-          </a>
-          <a class="item" :class="{active: reposFilter === 'mirrors'}" @click="changeReposFilter('mirrors')" v-if="isMirrorsEnabled">
-            {{ textMirrors }}
-            <div v-show="reposFilter === 'mirrors'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
-          </a>
-          <a class="item" :class="{active: reposFilter === 'collaborative'}" @click="changeReposFilter('collaborative')">
-            {{ textCollaborative }}
-            <div v-show="reposFilter === 'collaborative'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
-          </a>
-        </div>
+        <overflow-menu class="ui secondary pointing tabular borderless menu repos-filter">
+          <div class="overflow-menu-items tw-justify-center">
+            <a class="item" tabindex="0" :class="{active: reposFilter === 'all'}" @click="changeReposFilter('all')">
+              {{ textAll }}
+              <div v-show="reposFilter === 'all'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+            </a>
+            <a class="item" tabindex="0" :class="{active: reposFilter === 'sources'}" @click="changeReposFilter('sources')">
+              {{ textSources }}
+              <div v-show="reposFilter === 'sources'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+            </a>
+            <a class="item" tabindex="0" :class="{active: reposFilter === 'forks'}" @click="changeReposFilter('forks')">
+              {{ textForks }}
+              <div v-show="reposFilter === 'forks'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+            </a>
+            <a class="item" tabindex="0" :class="{active: reposFilter === 'mirrors'}" @click="changeReposFilter('mirrors')" v-if="isMirrorsEnabled">
+              {{ textMirrors }}
+              <div v-show="reposFilter === 'mirrors'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+            </a>
+            <a class="item" tabindex="0" :class="{active: reposFilter === 'collaborative'}" @click="changeReposFilter('collaborative')">
+              {{ textCollaborative }}
+              <div v-show="reposFilter === 'collaborative'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+            </a>
+          </div>
+        </overflow-menu>
       </div>
-      <div v-if="repos.length" class="ui attached table segment gt-rounded-bottom">
+      <div v-if="repos.length" class="ui attached table segment tw-rounded-b">
         <ul class="repo-owner-name-list">
-          <li class="gt-df gt-ac gt-py-3" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id">
+          <li class="tw-flex tw-items-center tw-py-2" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id">
             <a class="repo-list-link muted" :href="repo.link">
               <svg-icon :name="repoIcon(repo)" :size="16" class-name="repo-list-icon"/>
               <div class="text truncate">{{ repo.full_name }}</div>
@@ -417,56 +419,57 @@ export default sfc; // activate the IDE's Vue plugin
                 <svg-icon name="octicon-archive" :size="16"/>
               </div>
             </a>
-            <a class="gt-df gt-ac" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link" :data-tooltip-content="repo.locale_latest_commit_status_state">
+            <a class="tw-flex tw-items-center" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link" :data-tooltip-content="repo.locale_latest_commit_status_state">
               <!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl -->
-              <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class-name="'gt-ml-3 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/>
+              <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class-name="'tw-ml-2 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/>
             </a>
           </li>
         </ul>
-        <div v-if="showMoreReposLink" class="center gt-py-3 gt-border-secondary-top">
-          <div class="ui borderless pagination menu narrow">
+        <div v-if="showMoreReposLink" class="tw-text-center">
+          <div class="divider tw-my-0"/>
+          <div class="ui borderless pagination menu narrow tw-my-2">
             <a
-              class="item navigation gt-py-2" :class="{'disabled': page === 1}"
+              class="item navigation tw-py-1" :class="{'disabled': page === 1}"
               @click="changePage(1)" :title="textFirstPage"
             >
-              <svg-icon name="gitea-double-chevron-left" :size="16" class-name="gt-mr-2"/>
+              <svg-icon name="gitea-double-chevron-left" :size="16" class-name="tw-mr-1"/>
             </a>
             <a
-              class="item navigation gt-py-2" :class="{'disabled': page === 1}"
+              class="item navigation tw-py-1" :class="{'disabled': page === 1}"
               @click="changePage(page - 1)" :title="textPreviousPage"
             >
-              <svg-icon name="octicon-chevron-left" :size="16" clsas-name="gt-mr-2"/>
+              <svg-icon name="octicon-chevron-left" :size="16" clsas-name="tw-mr-1"/>
             </a>
-            <a class="active item gt-py-2">{{ page }}</a>
+            <a class="active item tw-py-1">{{ page }}</a>
             <a
               class="item navigation" :class="{'disabled': page === finalPage}"
               @click="changePage(page + 1)" :title="textNextPage"
             >
-              <svg-icon name="octicon-chevron-right" :size="16" class-name="gt-ml-2"/>
+              <svg-icon name="octicon-chevron-right" :size="16" class-name="tw-ml-1"/>
             </a>
             <a
-              class="item navigation gt-py-2" :class="{'disabled': page === finalPage}"
+              class="item navigation tw-py-1" :class="{'disabled': page === finalPage}"
               @click="changePage(finalPage)" :title="textLastPage"
             >
-              <svg-icon name="gitea-double-chevron-right" :size="16" class-name="gt-ml-2"/>
+              <svg-icon name="gitea-double-chevron-right" :size="16" class-name="tw-ml-1"/>
             </a>
           </div>
         </div>
       </div>
     </div>
     <div v-if="!isOrganization" v-show="tab === 'organizations'" class="ui tab active list dashboard-orgs">
-      <h4 class="ui top attached header gt-df gt-ac">
-        <div class="gt-f1 gt-df gt-ac">
+      <h4 class="ui top attached header tw-flex tw-items-center">
+        <div class="tw-flex-1 tw-flex tw-items-center">
           {{ textMyOrgs }}
-          <span class="ui grey label gt-ml-3">{{ organizationsTotalCount }}</span>
+          <span class="ui grey label tw-ml-2">{{ organizationsTotalCount }}</span>
         </div>
-        <a class="gt-df gt-ac muted" v-if="canCreateOrganization" :href="subUrl + '/org/create'" :data-tooltip-content="textNewOrg">
+        <a class="tw-flex tw-items-center muted" v-if="canCreateOrganization" :href="subUrl + '/org/create'" :data-tooltip-content="textNewOrg">
           <svg-icon name="octicon-plus"/>
         </a>
       </h4>
-      <div v-if="organizations.length" class="ui attached table segment gt-rounded-bottom">
+      <div v-if="organizations.length" class="ui attached table segment tw-rounded-b">
         <ul class="repo-owner-name-list">
-          <li class="gt-df gt-ac gt-py-3" v-for="org in organizations" :key="org.name">
+          <li class="tw-flex tw-items-center tw-py-2" v-for="org in organizations" :key="org.name">
             <a class="repo-list-link muted" :href="subUrl + '/' + encodeURIComponent(org.name)">
               <svg-icon name="octicon-organization" :size="16" class-name="repo-list-icon"/>
               <div class="text truncate">{{ org.name }}</div>
@@ -476,9 +479,9 @@ export default sfc; // activate the IDE's Vue plugin
                 </span>
               </div>
             </a>
-            <div class="text light grey gt-df gt-ac gt-ml-3">
+            <div class="text light grey tw-flex tw-items-center tw-ml-2">
               {{ org.num_repos }}
-              <svg-icon name="octicon-repo" :size="16" class-name="gt-ml-2 gt-mt-1"/>
+              <svg-icon name="octicon-repo" :size="16" class-name="tw-ml-1 tw-mt-0.5"/>
             </div>
           </li>
         </ul>
@@ -501,6 +504,22 @@ ul li:not(:last-child) {
   border-bottom: 1px solid var(--color-secondary);
 }
 
+.repos-search {
+  padding-bottom: 0 !important;
+}
+
+.repos-filter {
+  padding-top: 0 !important;
+  margin-top: 0 !important;
+  border-bottom-width: 0 !important;
+  margin-bottom: 2px !important;
+}
+
+.repos-filter .item {
+  padding-left: 6px !important;
+  padding-right: 6px !important;
+}
+
 .repo-list-link {
   min-width: 0; /* for text truncation */
   display: flex;
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
index 54877a18c0..352d085731 100644
--- a/web_src/js/components/DiffCommitSelector.vue
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -14,7 +14,7 @@ export default {
       },
       commits: [],
       hoverActivated: false,
-      lastReviewCommitSha: null
+      lastReviewCommitSha: null,
     };
   },
   computed: {
@@ -29,7 +29,7 @@ export default {
     },
     issueLink() {
       return this.$el.parentNode.getAttribute('data-issuelink');
-    }
+    },
   },
   mounted() {
     document.body.addEventListener('click', this.onBodyClick);
@@ -103,7 +103,7 @@ export default {
       this.menuVisible = !this.menuVisible;
       // load our commits when the menu is not yet visible (it'll be toggled after loading)
       // and we got no commits
-      if (this.commits.length === 0 && this.menuVisible && !this.isLoading) {
+      if (!this.commits.length && this.menuVisible && !this.isLoading) {
         this.isLoading = true;
         try {
           await this.fetchCommits();
@@ -185,7 +185,7 @@ export default {
         }
       }
     },
-  }
+  },
 };
 </script>
 <template>
@@ -204,19 +204,19 @@ export default {
     </button>
     <div class="menu left transition" id="diff-commit-selector-menu" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak :aria-expanded="menuVisible ? 'true': 'false'">
       <div class="loading-indicator is-loading" v-if="isLoading"/>
-      <div v-if="!isLoading" class="vertical item gt-df gt-fc gt-gap-2" id="diff-commit-list-show-all" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()">
+      <div v-if="!isLoading" class="vertical item" id="diff-commit-list-show-all" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()">
         <div class="gt-ellipsis">
           {{ locale.show_all_commits }}
         </div>
-        <div class="gt-ellipsis text light-2 gt-mb-0">
+        <div class="gt-ellipsis text light-2 tw-mb-0">
           {{ locale.stats_num_commits }}
         </div>
       </div>
       <!-- only show the show changes since last review if there is a review AND we are commits ahead of the last review -->
       <div
         v-if="lastReviewCommitSha != null" role="menuitem"
-        class="vertical item gt-df gt-fc gt-gap-2 gt-border-secondary-top"
-        :class="{disabled: commitsSinceLastReview === 0}"
+        class="vertical item"
+        :class="{disabled: !commitsSinceLastReview}"
         @keydown.enter="changesSinceLastReviewClick()"
         @click="changesSinceLastReviewClick()"
       >
@@ -227,10 +227,10 @@ export default {
           {{ commitsSinceLastReview }} commits
         </div>
       </div>
-      <span v-if="!isLoading" class="info gt-border-secondary-top text light-2">{{ locale.select_commit_hold_shift_for_range }}</span>
+      <span v-if="!isLoading" class="info text light-2">{{ locale.select_commit_hold_shift_for_range }}</span>
       <template v-for="commit in commits" :key="commit.id">
         <div
-          class="vertical item gt-df gt-gap-2 gt-border-secondary-top" role="menuitem"
+          class="vertical item" role="menuitem"
           :class="{selection: commit.selected, hovered: commit.hovered}"
           @keydown.enter.exact="commitClicked(commit.id)"
           @keydown.enter.shift.exact="commitClickedShift(commit)"
@@ -240,7 +240,7 @@ export default {
           @click.meta.exact="commitClicked(commit.id, true)"
           @click.shift.exact.stop.prevent="commitClickedShift(commit)"
         >
-          <div class="gt-f1 gt-df gt-fc gt-gap-2">
+          <div class="tw-flex-1 tw-flex tw-flex-col tw-gap-1">
             <div class="gt-ellipsis commit-list-summary">
               {{ commit.summary }}
             </div>
@@ -248,11 +248,11 @@ export default {
               {{ commit.committer_or_author_name }}
               <span class="text right">
                 <!-- TODO: make this respect the PreferredTimestampTense setting -->
-                <relative-time class="time-since" prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time>
+                <relative-time prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time>
               </span>
             </div>
           </div>
-          <div class="gt-mono">
+          <div class="tw-font-mono">
             {{ commit.short_sha }}
           </div>
         </div>
@@ -285,10 +285,14 @@ export default {
     width: 350px;
   }
 
-  #diff-commit-selector-menu .item {
+  #diff-commit-selector-menu .item,
+  #diff-commit-selector-menu .info {
+    display: flex !important;
     flex-direction: row;
     line-height: 1.4;
     padding: 7px 14px !important;
+    border-top: 1px solid var(--color-secondary) !important;
+    gap: 0.25em;
   }
 
   #diff-commit-selector-menu .item:focus {
diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue
index 8bde61804f..916780d913 100644
--- a/web_src/js/components/DiffFileList.vue
+++ b/web_src/js/components/DiffFileList.vue
@@ -31,26 +31,26 @@ export default {
     },
     loadMoreData() {
       loadMoreFiles(this.store.linkLoadMore);
-    }
+    },
   },
 };
 </script>
 <template>
-  <ol class="diff-stats gt-m-0" ref="root" v-if="store.fileListIsVisible">
+  <ol class="diff-stats tw-m-0" ref="root" v-if="store.fileListIsVisible">
     <li v-for="file in store.files" :key="file.NameHash">
-      <div class="gt-font-semibold gt-df gt-ac pull-right">
-        <span v-if="file.IsBin" class="gt-ml-1 gt-mr-3">{{ store.binaryFileMessage }}</span>
+      <div class="tw-font-semibold tw-flex tw-items-center pull-right">
+        <span v-if="file.IsBin" class="tw-ml-0.5 tw-mr-2">{{ store.binaryFileMessage }}</span>
         {{ file.IsBin ? '' : file.Addition + file.Deletion }}
-        <span v-if="!file.IsBin" class="diff-stats-bar gt-mx-3" :data-tooltip-content="store.statisticsMessage.replace('%d', (file.Addition + file.Deletion)).replace('%d', file.Addition).replace('%d', file.Deletion)">
+        <span v-if="!file.IsBin" class="diff-stats-bar tw-mx-2" :data-tooltip-content="store.statisticsMessage.replace('%d', (file.Addition + file.Deletion)).replace('%d', file.Addition).replace('%d', file.Deletion)">
           <div class="diff-stats-add-bar" :style="{ 'width': diffStatsWidth(file.Addition, file.Deletion) }"/>
         </span>
       </div>
       <!-- todo finish all file status, now modify, add, delete and rename -->
       <span :class="['status', diffTypeToString(file.Type)]" :data-tooltip-content="diffTypeToString(file.Type)">&nbsp;</span>
-      <a class="file gt-mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a>
+      <a class="file tw-font-mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a>
     </li>
-    <li v-if="store.isIncomplete" class="gt-pt-2">
-      <span class="file gt-df gt-ac gt-sb">{{ store.tooManyFilesMessage }}
+    <li v-if="store.isIncomplete" class="tw-pt-1">
+      <span class="file tw-flex tw-items-center tw-justify-between">{{ store.tooManyFilesMessage }}
         <a :class="['ui', 'basic', 'tiny', 'button', store.isLoadingNewData ? 'disabled' : '']" @click.stop="loadMoreData">{{ store.showMoreMessage }}</a>
       </span>
     </li>
diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue
index 3686629df8..cddfee1e04 100644
--- a/web_src/js/components/DiffFileTree.vue
+++ b/web_src/js/components/DiffFileTree.vue
@@ -30,7 +30,7 @@ export default {
           let newParent = {
             name: split,
             children: [],
-            isFile
+            isFile,
           };
 
           if (isFile === true) {
@@ -40,7 +40,7 @@ export default {
           if (parent) {
             // check if the folder already exists
             const existingFolder = parent.children.find(
-              (x) => x.name === split
+              (x) => x.name === split,
             );
             if (existingFolder) {
               newParent = existingFolder;
@@ -74,7 +74,7 @@ export default {
       // reduce the depth of our tree.
       mergeChildIfOnlyOneDir(result);
       return result;
-    }
+    },
   },
   mounted() {
     // Default to true if unset
@@ -129,7 +129,7 @@ export default {
   <div v-if="store.fileTreeIsVisible" class="diff-file-tree-items">
     <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
     <DiffFileTreeItem v-for="item in fileTree" :key="item.name" :item="item"/>
-    <div v-if="store.isIncomplete" class="gt-pt-2">
+    <div v-if="store.isIncomplete" class="tw-pt-1">
       <a :class="['ui', 'basic', 'tiny', 'button', store.isLoadingNewData ? 'disabled' : '']" @click.stop="loadMoreData">{{ store.showMoreMessage }}</a>
     </div>
   </div>
diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue
index 9d7ab4afc5..0f6e54363f 100644
--- a/web_src/js/components/DiffFileTreeItem.vue
+++ b/web_src/js/components/DiffFileTreeItem.vue
@@ -7,7 +7,7 @@ export default {
   props: {
     item: {
       type: Object,
-      required: true
+      required: true,
     },
   },
   data: () => ({
@@ -37,7 +37,7 @@ export default {
   >
     <!-- file -->
     <SvgIcon name="octicon-file"/>
-    <span class="gt-ellipsis gt-f1">{{ item.name }}</span>
+    <span class="gt-ellipsis tw-flex-1">{{ item.name }}</span>
     <SvgIcon :name="getIconForDiffType(item.file.Type).name" :class="getIconForDiffType(item.file.Type).classes"/>
   </a>
   <div v-else class="item-directory" :title="item.name" @click.stop="collapsed = !collapsed">
diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue
index b0b10b6252..bd0901a7b5 100644
--- a/web_src/js/components/PullRequestMergeForm.vue
+++ b/web_src/js/components/PullRequestMergeForm.vue
@@ -43,7 +43,7 @@ export default {
       for (const elem of document.querySelectorAll('[data-pull-merge-style]')) {
         toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
       }
-    }
+    },
   },
   created() {
     this.mergeStyleAllowedCount = this.mergeForm.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0);
@@ -94,6 +94,7 @@ export default {
     <!-- eslint-disable-next-line vue/no-v-html -->
     <div v-if="mergeForm.hasPendingPullRequestMerge" v-html="mergeForm.hasPendingPullRequestMergeTip" class="ui info message"/>
 
+    <!-- another similar form is in pull.tmpl (manual merge)-->
     <form class="ui form form-fetch-action" v-if="showActionForm" :action="mergeForm.baseLink+'/merge'" method="post">
       <input type="hidden" name="_csrf" :value="csrfToken">
       <input type="hidden" name="head_commit_id" v-model="mergeForm.pullHeadCommitID">
@@ -107,7 +108,7 @@ export default {
         <div class="field">
           <textarea name="merge_message_field" rows="5" :placeholder="mergeForm.mergeMessageFieldPlaceHolder" v-model="mergeMessageFieldValue"/>
           <template v-if="mergeMessageFieldValue !== mergeForm.defaultMergeMessage">
-            <button @click.prevent="clearMergeMessage" class="btn gt-mt-2 gt-p-2 interact-fg" :data-tooltip-content="mergeForm.textClearMergeMessageHint">
+            <button @click.prevent="clearMergeMessage" class="btn tw-mt-1 tw-p-1 interact-fg" :data-tooltip-content="mergeForm.textClearMergeMessageHint">
               {{ mergeForm.textClearMergeMessage }}
             </button>
           </template>
@@ -129,13 +130,13 @@ export default {
         {{ mergeForm.textCancel }}
       </button>
 
-      <div class="ui checkbox gt-ml-2" v-if="mergeForm.isPullBranchDeletable && !autoMergeWhenSucceed">
+      <div class="ui checkbox tw-ml-1" v-if="mergeForm.isPullBranchDeletable && !autoMergeWhenSucceed">
         <input name="delete_branch_after_merge" type="checkbox" v-model="deleteBranchAfterMerge" id="delete-branch-after-merge">
         <label for="delete-branch-after-merge">{{ mergeForm.textDeleteBranch }}</label>
       </div>
     </form>
 
-    <div v-if="!showActionForm" class="gt-df">
+    <div v-if="!showActionForm" class="tw-flex">
       <!-- the merge button -->
       <div class="ui buttons merge-button" :class="[mergeForm.emptyCommit ? 'grey' : mergeForm.allOverridableChecksOk ? 'primary' : 'red']" @click="toggleActionForm(true)">
         <button class="ui button">
@@ -176,7 +177,7 @@ export default {
       </div>
 
       <!-- the cancel auto merge button -->
-      <form v-if="mergeForm.hasPendingPullRequestMerge" :action="mergeForm.baseLink+'/cancel_auto_merge'" method="post" class="gt-ml-4">
+      <form v-if="mergeForm.hasPendingPullRequestMerge" :action="mergeForm.baseLink+'/cancel_auto_merge'" method="post" class="tw-ml-4">
         <input type="hidden" name="_csrf" :value="csrfToken">
         <button class="ui button">
           {{ mergeForm.textAutoMergeCancelSchedule }}
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 797869b78c..06c42f0b35 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -3,9 +3,9 @@ import {SvgIcon} from '../svg.js';
 import ActionRunStatus from './ActionRunStatus.vue';
 import {createApp} from 'vue';
 import {toggleElem} from '../utils/dom.js';
-import {getCurrentLocale} from '../utils.js';
+import {formatDatetime} from '../utils/time.js';
 import {renderAnsi} from '../render/ansi.js';
-import {POST} from '../modules/fetch.js';
+import {GET, POST, DELETE} from '../modules/fetch.js';
 
 const sfc = {
   name: 'RepoActionView',
@@ -66,7 +66,7 @@ const sfc = {
             name: '',
             link: '',
           },
-        }
+        },
       },
       currentJob: {
         title: '',
@@ -167,7 +167,7 @@ const sfc = {
       const logTimeStamp = document.createElement('span');
       logTimeStamp.className = 'log-time-stamp';
       const date = new Date(parseFloat(line.timestamp * 1000));
-      const timeStamp = date.toLocaleString(getCurrentLocale(), {timeZoneName: 'short'});
+      const timeStamp = formatDatetime(date);
       logTimeStamp.textContent = timeStamp;
       toggleElem(logTimeStamp, this.timeVisible['log-time-stamp']);
       // for "Show seconds"
@@ -196,10 +196,16 @@ const sfc = {
     },
 
     async fetchArtifacts() {
-      const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/artifacts`);
+      const resp = await GET(`${this.actionsURL}/runs/${this.runIndex}/artifacts`);
       return await resp.json();
     },
 
+    async deleteArtifact(name) {
+      if (!window.confirm(this.locale.confirmDeleteArtifact.replace('%s', name))) return;
+      await DELETE(`${this.run.link}/artifacts/${name}`);
+      await this.loadJob();
+    },
+
     async fetchJob() {
       const logCursors = this.currentJobStepsStates.map((it, idx) => {
         // cursor is used to indicate the last position of the logs
@@ -262,6 +268,10 @@ const sfc = {
       return ['success', 'skipped', 'failure', 'cancelled'].includes(status);
     },
 
+    isExpandable(status) {
+      return ['success', 'running', 'failure', 'cancelled'].includes(status);
+    },
+
     closeDropdown() {
       if (this.menuVisible) this.menuVisible = false;
     },
@@ -305,7 +315,7 @@ const sfc = {
       const logLine = this.$refs.steps.querySelector(selectedLogStep);
       if (!logLine) return;
       logLine.querySelector('.line-num').click();
-    }
+    },
   },
 };
 
@@ -329,6 +339,8 @@ export function initRepositoryActionView() {
       cancel: el.getAttribute('data-locale-cancel'),
       rerun: el.getAttribute('data-locale-rerun'),
       artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
+      areYouSure: el.getAttribute('data-locale-are-you-sure'),
+      confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'),
       rerun_all: el.getAttribute('data-locale-rerun-all'),
       showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
       showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
@@ -344,7 +356,7 @@ export function initRepositoryActionView() {
         skipped: el.getAttribute('data-locale-status-skipped'),
         blocked: el.getAttribute('data-locale-status-blocked'),
       },
-    }
+    },
   });
   view.mount(el);
 }
@@ -365,7 +377,7 @@ export function initRepositoryActionView() {
         <button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel">
           {{ locale.cancel }}
         </button>
-        <button class="ui basic small compact button gt-mr-0 link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
+        <button class="ui basic small compact button tw-mr-0 link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
           {{ locale.rerun_all }}
         </button>
       </div>
@@ -386,10 +398,10 @@ export function initRepositoryActionView() {
             <a class="job-brief-item" :href="run.link+'/jobs/'+index" :class="parseInt(jobIndex) === index ? 'selected' : ''" v-for="(job, index) in run.jobs" :key="job.id" @mouseenter="onHoverRerunIndex = job.id" @mouseleave="onHoverRerunIndex = -1">
               <div class="job-brief-item-left">
                 <ActionRunStatus :locale-status="locale.status[job.status]" :status="job.status"/>
-                <span class="job-brief-name gt-mx-3 gt-ellipsis">{{ job.name }}</span>
+                <span class="job-brief-name tw-mx-2 gt-ellipsis">{{ job.name }}</span>
               </div>
               <span class="job-brief-item-right">
-                <SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun gt-mx-3 link-action" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun && onHoverRerunIndex === job.id"/>
+                <SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun && onHoverRerunIndex === job.id"/>
                 <span class="step-summary-duration">{{ job.duration }}</span>
               </span>
             </a>
@@ -404,6 +416,9 @@ export function initRepositoryActionView() {
               <a class="job-artifacts-link" target="_blank" :href="run.link+'/artifacts/'+artifact.name">
                 <SvgIcon name="octicon-file" class="ui text black job-artifacts-icon"/>{{ artifact.name }}
               </a>
+              <a v-if="run.canDeleteArtifact" @click="deleteArtifact(artifact.name)" class="job-artifacts-delete">
+                <SvgIcon name="octicon-trash" class="ui text black job-artifacts-icon"/>
+              </a>
             </li>
           </ul>
         </div>
@@ -421,7 +436,7 @@ export function initRepositoryActionView() {
           </div>
           <div class="job-info-header-right">
             <div class="ui top right pointing dropdown custom jump item" @click.stop="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
-              <button class="btn gt-interact-bg gt-p-3">
+              <button class="btn gt-interact-bg tw-p-2">
                 <SvgIcon name="octicon-gear" :size="18"/>
               </button>
               <div class="menu transition action-job-menu" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
@@ -438,7 +453,7 @@ export function initRepositoryActionView() {
                   {{ locale.showFullScreen }}
                 </a>
                 <div class="divider"/>
-                <a :class="['item', currentJob.steps.length === 0 ? 'disabled' : '']" :href="run.link+'/jobs/'+jobIndex+'/logs'" target="_blank">
+                <a :class="['item', !currentJob.steps.length ? 'disabled' : '']" :href="run.link+'/jobs/'+jobIndex+'/logs'" target="_blank">
                   <i class="icon"><SvgIcon name="octicon-download"/></i>
                   {{ locale.downloadLogs }}
                 </a>
@@ -446,15 +461,15 @@ export function initRepositoryActionView() {
             </div>
           </div>
         </div>
-        <div class="job-step-container" ref="steps">
+        <div class="job-step-container" ref="steps" v-if="currentJob.steps.length">
           <div class="job-step-section" v-for="(jobStep, i) in currentJob.steps" :key="i">
-            <div class="job-step-summary" @click.stop="toggleStepLogs(i)" :class="currentJobStepsStates[i].expanded ? 'selected' : ''">
+            <div class="job-step-summary" @click.stop="isExpandable(jobStep.status) && toggleStepLogs(i)" :class="[currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']">
               <!-- If the job is done and the job step log is loaded for the first time, show the loading icon
                 currentJobStepsStates[i].cursor === null means the log is loaded for the first time
               -->
-              <SvgIcon v-if="isDone(run.status) && currentJobStepsStates[i].expanded && currentJobStepsStates[i].cursor === null" name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
-              <SvgIcon v-else :name="currentJobStepsStates[i].expanded ? 'octicon-chevron-down': 'octicon-chevron-right'" class="gt-mr-3"/>
-              <ActionRunStatus :status="jobStep.status" class="gt-mr-3"/>
+              <SvgIcon v-if="isDone(run.status) && currentJobStepsStates[i].expanded && currentJobStepsStates[i].cursor === null" name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
+              <SvgIcon v-else :name="currentJobStepsStates[i].expanded ? 'octicon-chevron-down': 'octicon-chevron-right'" :class="['tw-mr-2', !isExpandable(jobStep.status) && 'tw-invisible']"/>
+              <ActionRunStatus :status="jobStep.status" class="tw-mr-2"/>
 
               <span class="step-summary-msg gt-ellipsis">{{ jobStep.summary }}</span>
               <span class="step-summary-duration">{{ jobStep.duration }}</span>
@@ -502,8 +517,16 @@ export function initRepositoryActionView() {
 
 .action-commit-summary {
   display: flex;
+  flex-wrap: wrap;
   gap: 5px;
-  margin: 0 0 0 28px;
+  margin-left: 28px;
+}
+
+@media (max-width: 767.98px) {
+  .action-commit-summary {
+    margin-left: 0;
+    margin-top: 8px;
+  }
 }
 
 /* ================ */
@@ -513,9 +536,17 @@ export function initRepositoryActionView() {
   width: 30%;
   max-width: 400px;
   position: sticky;
-  top: 0;
+  top: 12px;
   max-height: 100vh;
   overflow-y: auto;
+  background: var(--color-body);
+  z-index: 2; /* above .job-info-header */
+}
+
+@media (max-width: 767.98px) {
+  .action-view-left {
+    position: static; /* can not sticky because multiple jobs would overlap into right view */
+  }
 }
 
 .job-artifacts-title {
@@ -528,6 +559,8 @@ export function initRepositoryActionView() {
 .job-artifacts-item {
   margin: 5px 0;
   padding: 6px;
+  display: flex;
+  justify-content: space-between;
 }
 
 .job-artifacts-list {
@@ -609,6 +642,10 @@ export function initRepositoryActionView() {
   width: 70%;
   display: flex;
   flex-direction: column;
+  border: 1px solid var(--color-console-border);
+  border-radius: var(--border-radius);
+  background: var(--color-console-bg);
+  align-self: flex-start;
 }
 
 /* begin fomantic button overrides */
@@ -668,13 +705,16 @@ export function initRepositoryActionView() {
   justify-content: space-between;
   align-items: center;
   padding: 0 12px;
-  border-bottom: 1px solid var(--color-console-border);
-  background-color: var(--color-console-bg);
   position: sticky;
   top: 0;
-  border-radius: var(--border-radius) var(--border-radius) 0 0;
   height: 60px;
-  z-index: 1;
+  z-index: 1; /* above .job-step-container */
+  background: var(--color-console-bg);
+  border-radius: 3px;
+}
+
+.job-info-header:has(+ .job-step-container) {
+  border-radius: var(--border-radius) var(--border-radius) 0 0;
 }
 
 .job-info-header .job-info-header-title {
@@ -689,20 +729,28 @@ export function initRepositoryActionView() {
 }
 
 .job-step-container {
-  background-color: var(--color-console-bg);
   max-height: 100%;
   border-radius: 0 0 var(--border-radius) var(--border-radius);
+  border-top: 1px solid var(--color-console-border);
   z-index: 0;
 }
 
 .job-step-container .job-step-summary {
-  cursor: pointer;
   padding: 5px 10px;
   display: flex;
   align-items: center;
   border-radius: var(--border-radius);
 }
 
+.job-step-container .job-step-summary.step-expandable {
+  cursor: pointer;
+}
+
+.job-step-container .job-step-summary.step-expandable:hover {
+  color: var(--color-console-fg);
+  background: var(--color-console-hover-bg);
+}
+
 .job-step-container .job-step-summary .step-summary-msg {
   flex: 1;
 }
@@ -711,12 +759,6 @@ export function initRepositoryActionView() {
   margin-left: 16px;
 }
 
-.job-step-container .job-step-summary:hover {
-  color: var(--color-console-fg);
-  background-color: var(--color-console-hover-bg);
-
-}
-
 .job-step-container .job-step-summary.selected {
   color: var(--color-console-fg);
   background-color: var(--color-console-active-bg);
@@ -724,17 +766,15 @@ export function initRepositoryActionView() {
   top: 60px;
 }
 
-@media (max-width: 768px) {
+@media (max-width: 767.98px) {
   .action-view-body {
     flex-direction: column;
   }
   .action-view-left, .action-view-right {
     width: 100%;
   }
-
   .action-view-left {
     max-width: none;
-    overflow-y: hidden;
   }
 }
 </style>
@@ -747,7 +787,7 @@ export function initRepositoryActionView() {
 
 @keyframes job-status-rotate-keyframes {
   100% {
-    transform: rotate(360deg);
+    transform: rotate(-360deg);
   }
 }
 
@@ -777,7 +817,7 @@ export function initRepositoryActionView() {
 /* class names 'log-time-seconds' and 'log-time-stamp' are used in the method toggleTimeDisplay */
 .job-log-line .line-num, .log-time-seconds {
   width: 48px;
-  color: var(--color-grey-light);
+  color: var(--color-text-light-3);
   text-align: right;
   user-select: none;
 }
@@ -793,7 +833,7 @@ export function initRepositoryActionView() {
 
 .job-log-line .log-time,
 .log-time-stamp {
-  color: var(--color-grey-light);
+  color: var(--color-text-light-3);
   margin-left: 10px;
   white-space: nowrap;
 }
diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue
index fe41218d88..a41fb61d78 100644
--- a/web_src/js/components/RepoActivityTopAuthors.vue
+++ b/web_src/js/components/RepoActivityTopAuthors.vue
@@ -47,7 +47,7 @@ const sfc = {
     this.colors.barColor = refStyle.backgroundColor;
     this.colors.textColor = refStyle.color;
     this.colors.textAltColor = refAltStyle.color;
-  }
+  },
 };
 
 export function initRepoActivityTopAuthorsChart() {
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index bc7d979d99..4e977ab185 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -19,24 +19,26 @@ const sfc = {
       });
 
       // TODO: fix this anti-pattern: side-effects-in-computed-properties
-      this.active = (items.length === 0 && this.showCreateNewBranch ? 0 : -1);
+      this.active = !items.length && this.showCreateNewBranch ? 0 : -1;
       return items;
     },
     showNoResults() {
-      return this.filteredItems.length === 0 && !this.showCreateNewBranch;
+      return !this.filteredItems.length && !this.showCreateNewBranch;
     },
     showCreateNewBranch() {
       if (this.disableCreateBranch || !this.searchTerm) {
         return false;
       }
-      return this.items.filter((item) => item.name.toLowerCase() === this.searchTerm.toLowerCase()).length === 0;
+      return !this.items.filter((item) => {
+        return item.name.toLowerCase() === this.searchTerm.toLowerCase();
+      }).length;
     },
     formActionUrl() {
       return `${this.repoLink}/branches/_new/${this.branchNameSubURL}`;
     },
     shouldCreateTag() {
       return this.mode === 'tags';
-    }
+    },
   },
 
   watch: {
@@ -45,7 +47,7 @@ const sfc = {
         this.focusSearchField();
         this.fetchBranchesOrTags();
       }
-    }
+    },
   },
 
   beforeMount() {
@@ -83,7 +85,7 @@ const sfc = {
         this.isViewBranch = false;
         this.$refs.dropdownRefName.textContent = item.name;
         if (this.setAction) {
-          $(`#${this.branchForm}`).attr('action', url);
+          document.getElementById(this.branchForm)?.setAttribute('action', url);
         } else {
           $(`#${this.branchForm} input[name="refURL"]`).val(url);
         }
@@ -123,7 +125,7 @@ const sfc = {
       return -1;
     },
     scrollToActive() {
-      let el = this.$refs[`listItem${this.active}`];
+      let el = this.$refs[`listItem${this.active}`]; // eslint-disable-line no-jquery/variable-pattern
       if (!el || !el.length) return;
       if (Array.isArray(el)) {
         el = el[0];
@@ -209,7 +211,7 @@ const sfc = {
         this.isLoading = false;
       }
     },
-  }
+  },
 };
 
 export function initRepoBranchTagSelector(selector) {
@@ -245,13 +247,13 @@ export default sfc; // activate IDE's Vue plugin
 </script>
 <template>
   <div class="ui dropdown custom">
-    <button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df gt-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
-      <span class="text gt-df gt-ac gt-mr-2">
+    <button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex tw-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
+      <span class="text tw-flex tw-items-center tw-mr-1">
         <template v-if="release">{{ textReleaseCompare }}</template>
         <template v-else>
           <svg-icon v-if="isViewTag" name="octicon-tag"/>
           <svg-icon v-else name="octicon-git-branch"/>
-          <strong ref="dropdownRefName" class="gt-ml-3">{{ refNameText }}</strong>
+          <strong ref="dropdownRefName" class="tw-ml-2">{{ refNameText }}</strong>
         </template>
       </span>
       <svg-icon name="octicon-triangle-down" :size="14" class-name="dropdown icon"/>
@@ -263,10 +265,10 @@ export default sfc; // activate IDE's Vue plugin
       </div>
       <div v-if="showBranchesInDropdown" class="branch-tag-tab">
         <a class="branch-tag-item muted" :class="{active: mode === 'branches'}" href="#" @click="handleTabSwitch('branches')">
-          <svg-icon name="octicon-git-branch" :size="16" class-name="gt-mr-2"/>{{ textBranches }}
+          <svg-icon name="octicon-git-branch" :size="16" class-name="tw-mr-1"/>{{ textBranches }}
         </a>
         <a v-if="!noTag" class="branch-tag-item muted" :class="{active: mode === 'tags'}" href="#" @click="handleTabSwitch('tags')">
-          <svg-icon name="octicon-tag" :size="16" class-name="gt-mr-2"/>{{ textTags }}
+          <svg-icon name="octicon-tag" :size="16" class-name="tw-mr-1"/>{{ textTags }}
         </a>
       </div>
       <div class="branch-tag-divider"/>
@@ -278,7 +280,7 @@ export default sfc; // activate IDE's Vue plugin
           <div class="ui label" v-if="item.name===repoDefaultBranch && mode === 'branches'">
             {{ textDefaultBranchLabel }}
           </div>
-          <a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon gt-float-right" :href="rssURLPrefix + item.url" target="_blank" @click.stop>
+          <a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon tw-float-right" :href="rssURLPrefix + item.url" target="_blank" @click.stop>
             <!-- creating a lot of Vue component is pretty slow, so we use a static SVG here -->
             <svg width="14" height="14" class="svg octicon-rss"><use href="#svg-symbol-octicon-rss"/></svg>
           </a>
diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue
new file mode 100644
index 0000000000..adce431264
--- /dev/null
+++ b/web_src/js/components/RepoCodeFrequency.vue
@@ -0,0 +1,172 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {
+  Chart,
+  Legend,
+  LinearScale,
+  TimeScale,
+  PointElement,
+  LineElement,
+  Filler,
+} from 'chart.js';
+import {GET} from '../modules/fetch.js';
+import {Line as ChartLine} from 'vue-chartjs';
+import {
+  startDaysBetween,
+  firstStartDateAfterDate,
+  fillEmptyStartDaysWithZeroes,
+} from '../utils/time.js';
+import {chartJsColors} from '../utils/color.js';
+import {sleep} from '../utils.js';
+import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
+
+const {pageData} = window.config;
+
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
+
+Chart.register(
+  TimeScale,
+  LinearScale,
+  Legend,
+  PointElement,
+  LineElement,
+  Filler,
+);
+
+export default {
+  components: {ChartLine, SvgIcon},
+  props: {
+    locale: {
+      type: Object,
+      required: true,
+    },
+  },
+  data: () => ({
+    isLoading: false,
+    errorText: '',
+    repoLink: pageData.repoLink || [],
+    data: [],
+  }),
+  mounted() {
+    this.fetchGraphData();
+  },
+  methods: {
+    async fetchGraphData() {
+      this.isLoading = true;
+      try {
+        let response;
+        do {
+          response = await GET(`${this.repoLink}/activity/code-frequency/data`);
+          if (response.status === 202) {
+            await sleep(1000); // wait for 1 second before retrying
+          }
+        } while (response.status === 202);
+        if (response.ok) {
+          this.data = await response.json();
+          const weekValues = Object.values(this.data);
+          const start = weekValues[0].week;
+          const end = firstStartDateAfterDate(new Date());
+          const startDays = startDaysBetween(new Date(start), new Date(end));
+          this.data = fillEmptyStartDaysWithZeroes(startDays, this.data);
+          this.errorText = '';
+        } else {
+          this.errorText = response.statusText;
+        }
+      } catch (err) {
+        this.errorText = err.message;
+      } finally {
+        this.isLoading = false;
+      }
+    },
+
+    toGraphData(data) {
+      return {
+        datasets: [
+          {
+            data: data.map((i) => ({x: i.week, y: i.additions})),
+            pointRadius: 0,
+            pointHitRadius: 0,
+            fill: true,
+            label: 'Additions',
+            backgroundColor: chartJsColors['additions'],
+            borderWidth: 0,
+            tension: 0.3,
+          },
+          {
+            data: data.map((i) => ({x: i.week, y: -i.deletions})),
+            pointRadius: 0,
+            pointHitRadius: 0,
+            fill: true,
+            label: 'Deletions',
+            backgroundColor: chartJsColors['deletions'],
+            borderWidth: 0,
+            tension: 0.3,
+          },
+        ],
+      };
+    },
+
+    getOptions() {
+      return {
+        responsive: true,
+        maintainAspectRatio: false,
+        animation: true,
+        plugins: {
+          legend: {
+            display: true,
+          },
+        },
+        scales: {
+          x: {
+            type: 'time',
+            grid: {
+              display: false,
+            },
+            time: {
+              minUnit: 'month',
+            },
+            ticks: {
+              maxRotation: 0,
+              maxTicksLimit: 12,
+            },
+          },
+          y: {
+            ticks: {
+              maxTicksLimit: 6,
+            },
+          },
+        },
+      };
+    },
+  },
+};
+</script>
+<template>
+  <div>
+    <div class="ui header tw-flex tw-items-center tw-justify-between">
+      {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: `Code frequency over the history of ${repoLink.slice(1)}` }}
+    </div>
+    <div class="tw-flex ui segment main-graph">
+      <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
+        <div v-if="isLoading">
+          <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
+          {{ locale.loadingInfo }}
+        </div>
+        <div v-else class="text red">
+          <SvgIcon name="octicon-x-circle-fill"/>
+          {{ errorText }}
+        </div>
+      </div>
+      <ChartLine
+        v-memo="data" v-if="data.length !== 0"
+        :data="toGraphData(data)" :options="getOptions()"
+      />
+    </div>
+  </div>
+</template>
+<style scoped>
+.main-graph {
+  height: 440px;
+}
+</style>
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
new file mode 100644
index 0000000000..2347c41ae4
--- /dev/null
+++ b/web_src/js/components/RepoContributors.vue
@@ -0,0 +1,432 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {
+  Chart,
+  Title,
+  BarElement,
+  LinearScale,
+  TimeScale,
+  PointElement,
+  LineElement,
+  Filler,
+} from 'chart.js';
+import {GET} from '../modules/fetch.js';
+import zoomPlugin from 'chartjs-plugin-zoom';
+import {Line as ChartLine} from 'vue-chartjs';
+import {
+  startDaysBetween,
+  firstStartDateAfterDate,
+  fillEmptyStartDaysWithZeroes,
+} from '../utils/time.js';
+import {chartJsColors} from '../utils/color.js';
+import {sleep} from '../utils.js';
+import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
+import $ from 'jquery';
+
+const {pageData} = window.config;
+
+const customEventListener = {
+  id: 'customEventListener',
+  afterEvent: (chart, args, opts) => {
+    // event will be replayed from chart.update when reset zoom,
+    // so we need to check whether args.replay is true to avoid call loops
+    if (args.event.type === 'dblclick' && opts.chartType === 'main' && !args.replay) {
+      chart.resetZoom();
+      opts.instance.updateOtherCharts(args.event, true);
+    }
+  },
+};
+
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
+
+Chart.register(
+  TimeScale,
+  LinearScale,
+  BarElement,
+  Title,
+  PointElement,
+  LineElement,
+  Filler,
+  zoomPlugin,
+  customEventListener,
+);
+
+export default {
+  components: {ChartLine, SvgIcon},
+  props: {
+    locale: {
+      type: Object,
+      required: true,
+    },
+  },
+  data: () => ({
+    isLoading: false,
+    errorText: '',
+    totalStats: {},
+    sortedContributors: {},
+    repoLink: pageData.repoLink || [],
+    type: pageData.contributionType,
+    contributorsStats: [],
+    xAxisStart: null,
+    xAxisEnd: null,
+    xAxisMin: null,
+    xAxisMax: null,
+  }),
+  mounted() {
+    this.fetchGraphData();
+
+    $('#repo-contributors').dropdown({
+      onChange: (val) => {
+        this.xAxisMin = this.xAxisStart;
+        this.xAxisMax = this.xAxisEnd;
+        this.type = val;
+        this.sortContributors();
+      },
+    });
+  },
+  methods: {
+    sortContributors() {
+      const contributors = this.filterContributorWeeksByDateRange();
+      const criteria = `total_${this.type}`;
+      this.sortedContributors = Object.values(contributors)
+        .filter((contributor) => contributor[criteria] !== 0)
+        .sort((a, b) => a[criteria] > b[criteria] ? -1 : a[criteria] === b[criteria] ? 0 : 1)
+        .slice(0, 100);
+    },
+
+    async fetchGraphData() {
+      this.isLoading = true;
+      try {
+        let response;
+        do {
+          response = await GET(`${this.repoLink}/activity/contributors/data`);
+          if (response.status === 202) {
+            await sleep(1000); // wait for 1 second before retrying
+          }
+        } while (response.status === 202);
+        if (response.ok) {
+          const data = await response.json();
+          const {total, ...rest} = data;
+          // below line might be deleted if we are sure go produces map always sorted by keys
+          total.weeks = Object.fromEntries(Object.entries(total.weeks).sort());
+
+          const weekValues = Object.values(total.weeks);
+          this.xAxisStart = weekValues[0].week;
+          this.xAxisEnd = firstStartDateAfterDate(new Date());
+          const startDays = startDaysBetween(new Date(this.xAxisStart), new Date(this.xAxisEnd));
+          total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks);
+          this.xAxisMin = this.xAxisStart;
+          this.xAxisMax = this.xAxisEnd;
+          this.contributorsStats = {};
+          for (const [email, user] of Object.entries(rest)) {
+            user.weeks = fillEmptyStartDaysWithZeroes(startDays, user.weeks);
+            this.contributorsStats[email] = user;
+          }
+          this.sortContributors();
+          this.totalStats = total;
+          this.errorText = '';
+        } else {
+          this.errorText = response.statusText;
+        }
+      } catch (err) {
+        this.errorText = err.message;
+      } finally {
+        this.isLoading = false;
+      }
+    },
+
+    filterContributorWeeksByDateRange() {
+      const filteredData = {};
+      const data = this.contributorsStats;
+      for (const key of Object.keys(data)) {
+        const user = data[key];
+        user.total_commits = 0;
+        user.total_additions = 0;
+        user.total_deletions = 0;
+        user.max_contribution_type = 0;
+        const filteredWeeks = user.weeks.filter((week) => {
+          const oneWeek = 7 * 24 * 60 * 60 * 1000;
+          if (week.week >= this.xAxisMin - oneWeek && week.week <= this.xAxisMax + oneWeek) {
+            user.total_commits += week.commits;
+            user.total_additions += week.additions;
+            user.total_deletions += week.deletions;
+            if (week[this.type] > user.max_contribution_type) {
+              user.max_contribution_type = week[this.type];
+            }
+            return true;
+          }
+          return false;
+        });
+        // this line is required. See https://github.com/sahinakkaya/gitea/pull/3#discussion_r1396495722
+        // for details.
+        user.max_contribution_type += 1;
+
+        filteredData[key] = {...user, weeks: filteredWeeks};
+      }
+
+      return filteredData;
+    },
+
+    maxMainGraph() {
+      // This method calculates maximum value for Y value of the main graph. If the number
+      // of maximum contributions for selected contribution type is 15.955 it is probably
+      // better to round it up to 20.000.This method is responsible for doing that.
+      // Normally, chartjs handles this automatically, but it will resize the graph when you
+      // zoom, pan etc. I think resizing the graph makes it harder to compare things visually.
+      const maxValue = Math.max(
+        ...this.totalStats.weeks.map((o) => o[this.type]),
+      );
+      const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
+      if (coefficient % 1 === 0) return maxValue;
+      return (1 - (coefficient % 1)) * 10 ** exp + maxValue;
+    },
+
+    maxContributorGraph() {
+      // Similar to maxMainGraph method this method calculates maximum value for Y value
+      // for contributors' graph. If I let chartjs do this for me, it will choose different
+      // maxY value for each contributors' graph which again makes it harder to compare.
+      const maxValue = Math.max(
+        ...this.sortedContributors.map((c) => c.max_contribution_type),
+      );
+      const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
+      if (coefficient % 1 === 0) return maxValue;
+      return (1 - (coefficient % 1)) * 10 ** exp + maxValue;
+    },
+
+    toGraphData(data) {
+      return {
+        datasets: [
+          {
+            data: data.map((i) => ({x: i.week, y: i[this.type]})),
+            pointRadius: 0,
+            pointHitRadius: 0,
+            fill: 'start',
+            backgroundColor: chartJsColors[this.type],
+            borderWidth: 0,
+            tension: 0.3,
+          },
+        ],
+      };
+    },
+
+    updateOtherCharts(event, reset) {
+      const minVal = event.chart.options.scales.x.min;
+      const maxVal = event.chart.options.scales.x.max;
+      if (reset) {
+        this.xAxisMin = this.xAxisStart;
+        this.xAxisMax = this.xAxisEnd;
+        this.sortContributors();
+      } else if (minVal) {
+        this.xAxisMin = minVal;
+        this.xAxisMax = maxVal;
+        this.sortContributors();
+      }
+    },
+
+    getOptions(type) {
+      return {
+        responsive: true,
+        maintainAspectRatio: false,
+        animation: false,
+        events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove', 'dblclick'],
+        plugins: {
+          title: {
+            display: type === 'main',
+            text: 'drag: zoom, shift+drag: pan, double click: reset zoom',
+            position: 'top',
+            align: 'center',
+          },
+          customEventListener: {
+            chartType: type,
+            instance: this,
+          },
+          zoom: {
+            pan: {
+              enabled: true,
+              modifierKey: 'shift',
+              mode: 'x',
+              threshold: 20,
+              onPanComplete: this.updateOtherCharts,
+            },
+            limits: {
+              x: {
+                // Check https://www.chartjs.org/chartjs-plugin-zoom/latest/guide/options.html#scale-limits
+                // to know what each option means
+                min: 'original',
+                max: 'original',
+
+                // number of milliseconds in 2 weeks. Minimum x range will be 2 weeks when you zoom on the graph
+                minRange: 2 * 7 * 24 * 60 * 60 * 1000,
+              },
+            },
+            zoom: {
+              drag: {
+                enabled: type === 'main',
+              },
+              pinch: {
+                enabled: type === 'main',
+              },
+              mode: 'x',
+              onZoomComplete: this.updateOtherCharts,
+            },
+          },
+        },
+        scales: {
+          x: {
+            min: this.xAxisMin,
+            max: this.xAxisMax,
+            type: 'time',
+            grid: {
+              display: false,
+            },
+            time: {
+              minUnit: 'month',
+            },
+            ticks: {
+              maxRotation: 0,
+              maxTicksLimit: type === 'main' ? 12 : 6,
+            },
+          },
+          y: {
+            min: 0,
+            max: type === 'main' ? this.maxMainGraph() : this.maxContributorGraph(),
+            ticks: {
+              maxTicksLimit: type === 'main' ? 6 : 4,
+            },
+          },
+        },
+      };
+    },
+  },
+};
+</script>
+<template>
+  <div>
+    <div class="ui header tw-flex tw-items-center tw-justify-between">
+      <div>
+        <relative-time
+          v-if="xAxisMin > 0"
+          format="datetime"
+          year="numeric"
+          month="short"
+          day="numeric"
+          weekday=""
+          :datetime="new Date(xAxisMin)"
+        >
+          {{ new Date(xAxisMin) }}
+        </relative-time>
+        {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "-" }}
+        <relative-time
+          v-if="xAxisMax > 0"
+          format="datetime"
+          year="numeric"
+          month="short"
+          day="numeric"
+          weekday=""
+          :datetime="new Date(xAxisMax)"
+        >
+          {{ new Date(xAxisMax) }}
+        </relative-time>
+      </div>
+      <div>
+        <!-- Contribution type -->
+        <div class="ui dropdown jump" id="repo-contributors">
+          <div class="ui basic compact button">
+            <span class="text">
+              <span class="not-mobile">{{ locale.filterLabel }}&nbsp;</span><strong>{{ locale.contributionType[type] }}</strong>
+              <svg-icon name="octicon-triangle-down" :size="14"/>
+            </span>
+          </div>
+          <div class="menu">
+            <div :class="['item', {'active': type === 'commits'}]">
+              {{ locale.contributionType.commits }}
+            </div>
+            <div :class="['item', {'active': type === 'additions'}]">
+              {{ locale.contributionType.additions }}
+            </div>
+            <div :class="['item', {'active': type === 'deletions'}]">
+              {{ locale.contributionType.deletions }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="tw-flex ui segment main-graph">
+      <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
+        <div v-if="isLoading">
+          <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
+          {{ locale.loadingInfo }}
+        </div>
+        <div v-else class="text red">
+          <SvgIcon name="octicon-x-circle-fill"/>
+          {{ errorText }}
+        </div>
+      </div>
+      <ChartLine
+        v-memo="[totalStats.weeks, type]" v-if="Object.keys(totalStats).length !== 0"
+        :data="toGraphData(totalStats.weeks)" :options="getOptions('main')"
+      />
+    </div>
+    <div class="contributor-grid">
+      <div
+        v-for="(contributor, index) in sortedContributors"
+        :key="index"
+        v-memo="[sortedContributors, type]"
+      >
+        <div class="ui top attached header tw-flex tw-flex-1">
+          <b class="ui right">#{{ index + 1 }}</b>
+          <a :href="contributor.home_link">
+            <img class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link">
+          </a>
+          <div class="tw-ml-2">
+            <a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a>
+            <h4 v-else class="contributor-name">
+              {{ contributor.name }}
+            </h4>
+            <p class="tw-text-12 tw-flex tw-gap-1">
+              <strong v-if="contributor.total_commits">{{ contributor.total_commits.toLocaleString() }} {{ locale.contributionType.commits }}</strong>
+              <strong v-if="contributor.total_additions" class="text green">{{ contributor.total_additions.toLocaleString() }}++ </strong>
+              <strong v-if="contributor.total_deletions" class="text red">
+                {{ contributor.total_deletions.toLocaleString() }}--</strong>
+            </p>
+          </div>
+        </div>
+        <div class="ui attached segment">
+          <div>
+            <ChartLine
+              :data="toGraphData(contributor.weeks)"
+              :options="getOptions('contributor')"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<style scoped>
+.main-graph {
+  height: 260px;
+  padding-top: 2px;
+}
+
+.contributor-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 1rem;
+}
+
+.contributor-grid > * {
+  min-width: 0;
+}
+
+@media (max-width: 991.98px) {
+  .contributor-grid {
+    grid-template-columns: repeat(1, 1fr);
+  }
+}
+
+.contributor-name {
+  margin-bottom: 0;
+}
+</style>
diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue
new file mode 100644
index 0000000000..502af533da
--- /dev/null
+++ b/web_src/js/components/RepoRecentCommits.vue
@@ -0,0 +1,149 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {
+  Chart,
+  Tooltip,
+  BarElement,
+  LinearScale,
+  TimeScale,
+} from 'chart.js';
+import {GET} from '../modules/fetch.js';
+import {Bar} from 'vue-chartjs';
+import {
+  startDaysBetween,
+  firstStartDateAfterDate,
+  fillEmptyStartDaysWithZeroes,
+} from '../utils/time.js';
+import {chartJsColors} from '../utils/color.js';
+import {sleep} from '../utils.js';
+import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
+
+const {pageData} = window.config;
+
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
+
+Chart.register(
+  TimeScale,
+  LinearScale,
+  BarElement,
+  Tooltip,
+);
+
+export default {
+  components: {Bar, SvgIcon},
+  props: {
+    locale: {
+      type: Object,
+      required: true,
+    },
+  },
+  data: () => ({
+    isLoading: false,
+    errorText: '',
+    repoLink: pageData.repoLink || [],
+    data: [],
+  }),
+  mounted() {
+    this.fetchGraphData();
+  },
+  methods: {
+    async fetchGraphData() {
+      this.isLoading = true;
+      try {
+        let response;
+        do {
+          response = await GET(`${this.repoLink}/activity/recent-commits/data`);
+          if (response.status === 202) {
+            await sleep(1000); // wait for 1 second before retrying
+          }
+        } while (response.status === 202);
+        if (response.ok) {
+          const data = await response.json();
+          const start = Object.values(data)[0].week;
+          const end = firstStartDateAfterDate(new Date());
+          const startDays = startDaysBetween(new Date(start), new Date(end));
+          this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
+          this.errorText = '';
+        } else {
+          this.errorText = response.statusText;
+        }
+      } catch (err) {
+        this.errorText = err.message;
+      } finally {
+        this.isLoading = false;
+      }
+    },
+
+    toGraphData(data) {
+      return {
+        datasets: [
+          {
+            data: data.map((i) => ({x: i.week, y: i.commits})),
+            label: 'Commits',
+            backgroundColor: chartJsColors['commits'],
+            borderWidth: 0,
+            tension: 0.3,
+          },
+        ],
+      };
+    },
+
+    getOptions() {
+      return {
+        responsive: true,
+        maintainAspectRatio: false,
+        animation: true,
+        scales: {
+          x: {
+            type: 'time',
+            grid: {
+              display: false,
+            },
+            time: {
+              minUnit: 'week',
+            },
+            ticks: {
+              maxRotation: 0,
+              maxTicksLimit: 52,
+            },
+          },
+          y: {
+            ticks: {
+              maxTicksLimit: 6,
+            },
+          },
+        },
+      };
+    },
+  },
+};
+</script>
+<template>
+  <div>
+    <div class="ui header tw-flex tw-items-center tw-justify-between">
+      {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "Number of commits in the past year" }}
+    </div>
+    <div class="tw-flex ui segment main-graph">
+      <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
+        <div v-if="isLoading">
+          <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
+          {{ locale.loadingInfo }}
+        </div>
+        <div v-else class="text red">
+          <SvgIcon name="octicon-x-circle-fill"/>
+          {{ errorText }}
+        </div>
+      </div>
+      <Bar
+        v-memo="data" v-if="data.length !== 0"
+        :data="toGraphData(data)" :options="getOptions()"
+      />
+    </div>
+  </div>
+</template>
+<style scoped>
+.main-graph {
+  height: 250px;
+}
+</style>
diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue
index f6af7e447f..103cc525ad 100644
--- a/web_src/js/components/ScopedAccessTokenSelector.vue
+++ b/web_src/js/components/ScopedAccessTokenSelector.vue
@@ -39,7 +39,7 @@ const sfc = {
         'repository',
         'user');
       return categories;
-    }
+    },
   },
 
   mounted() {
@@ -68,7 +68,7 @@ const sfc = {
       }
       // no scopes selected, show validation error
       showElem(warningEl);
-    }
+    },
   },
 };
 
@@ -87,7 +87,7 @@ export function initScopedAccessTokenCategories() {
 
 </script>
 <template>
-  <div v-for="category in categories" :key="category" class="field gt-pl-2 gt-pb-2 access-token-category">
+  <div v-for="category in categories" :key="category" class="field tw-pl-1 tw-pb-1 access-token-category">
     <label class="category-label" :for="'access-token-scope-' + category">
       {{ category }}
     </label>
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 044976ea7b..f388b1122e 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -1,141 +1,169 @@
 import $ from 'jquery';
 import {checkAppUrl} from '../common-global.js';
 import {hideElem, showElem, toggleElem} from '../../utils/dom.js';
+import {POST} from '../../modules/fetch.js';
 
-const {csrfToken, appSubUrl} = window.config;
+const {appSubUrl} = window.config;
+
+function onSecurityProtocolChange() {
+  if (Number(document.getElementById('security_protocol')?.value) > 0) {
+    showElem('.has-tls');
+  } else {
+    hideElem('.has-tls');
+  }
+}
 
 export function initAdminCommon() {
-  if ($('.page-content.admin').length === 0) {
-    return;
-  }
+  if (!document.querySelector('.page-content.admin')) return;
 
   // check whether appUrl(ROOT_URL) is correct, if not, show an error message
   checkAppUrl();
 
   // New user
   if ($('.admin.new.user').length > 0 || $('.admin.edit.user').length > 0) {
-    $('#login_type').on('change', function () {
-      if ($(this).val().substring(0, 1) === '0') {
-        $('#user_name').removeAttr('disabled');
-        $('#login_name').removeAttr('required');
-        hideElem($('.non-local'));
-        showElem($('.local'));
-        $('#user_name').trigger('focus');
+    document.getElementById('login_type')?.addEventListener('change', function () {
+      if (this.value?.substring(0, 1) === '0') {
+        document.getElementById('user_name')?.removeAttribute('disabled');
+        document.getElementById('login_name')?.removeAttribute('required');
+        hideElem('.non-local');
+        showElem('.local');
+        document.getElementById('user_name')?.focus();
 
-        if ($(this).data('password') === 'required') {
-          $('#password').attr('required', 'required');
+        if (this.getAttribute('data-password') === 'required') {
+          document.getElementById('password')?.setAttribute('required', 'required');
         }
       } else {
-        if ($('.admin.edit.user').length > 0) {
-          $('#user_name').attr('disabled', 'disabled');
+        if (document.querySelector('.admin.edit.user')) {
+          document.getElementById('user_name')?.setAttribute('disabled', 'disabled');
         }
-        $('#login_name').attr('required', 'required');
-        showElem($('.non-local'));
-        hideElem($('.local'));
-        $('#login_name').trigger('focus');
+        document.getElementById('login_name')?.setAttribute('required', 'required');
+        showElem('.non-local');
+        hideElem('.local');
+        document.getElementById('login_name')?.focus();
 
-        $('#password').removeAttr('required');
+        document.getElementById('password')?.removeAttribute('required');
       }
     });
   }
 
-  function onSecurityProtocolChange() {
-    if ($('#security_protocol').val() > 0) {
-      showElem($('.has-tls'));
-    } else {
-      hideElem($('.has-tls'));
-    }
-  }
-
   function onUsePagedSearchChange() {
-    if ($('#use_paged_search').prop('checked')) {
+    const searchPageSizeElements = document.querySelectorAll('.search-page-size');
+    if (document.getElementById('use_paged_search').checked) {
       showElem('.search-page-size');
-      $('.search-page-size').find('input').attr('required', 'required');
+      for (const el of searchPageSizeElements) {
+        el.querySelector('input')?.setAttribute('required', 'required');
+      }
     } else {
       hideElem('.search-page-size');
-      $('.search-page-size').find('input').removeAttr('required');
+      for (const el of searchPageSizeElements) {
+        el.querySelector('input')?.removeAttribute('required');
+      }
     }
   }
 
   function onOAuth2Change(applyDefaultValues) {
-    hideElem($('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url'));
-    $('.open_id_connect_auto_discovery_url input[required]').removeAttr('required');
+    hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url');
+    for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input[required]')) {
+      input.removeAttribute('required');
+    }
 
-    const provider = $('#oauth2_provider').val();
+    const provider = document.getElementById('oauth2_provider')?.value;
     switch (provider) {
       case 'openidConnect':
-        $('.open_id_connect_auto_discovery_url input').attr('required', 'required');
-        showElem($('.open_id_connect_auto_discovery_url'));
+        for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input')) {
+          input.setAttribute('required', 'required');
+        }
+        showElem('.open_id_connect_auto_discovery_url');
         break;
       default:
-        if ($(`#${provider}_customURLSettings`).data('required')) {
-          $('#oauth2_use_custom_url').attr('checked', 'checked');
+        if (document.getElementById(`#${provider}_customURLSettings`)?.getAttribute('data-required')) {
+          document.getElementById('oauth2_use_custom_url')?.setAttribute('checked', 'checked');
         }
-        if ($(`#${provider}_customURLSettings`).data('available')) {
-          showElem($('.oauth2_use_custom_url'));
+        if (document.getElementById(`#${provider}_customURLSettings`)?.getAttribute('data-available')) {
+          showElem('.oauth2_use_custom_url');
         }
     }
     onOAuth2UseCustomURLChange(applyDefaultValues);
   }
 
   function onOAuth2UseCustomURLChange(applyDefaultValues) {
-    const provider = $('#oauth2_provider').val();
-    hideElem($('.oauth2_use_custom_url_field'));
-    $('.oauth2_use_custom_url_field input[required]').removeAttr('required');
+    const provider = document.getElementById('oauth2_provider')?.value;
+    hideElem('.oauth2_use_custom_url_field');
+    for (const input of document.querySelectorAll('.oauth2_use_custom_url_field input[required]')) {
+      input.removeAttribute('required');
+    }
 
-    if ($('#oauth2_use_custom_url').is(':checked')) {
+    if (document.getElementById('oauth2_use_custom_url')?.checked) {
       for (const custom of ['token_url', 'auth_url', 'profile_url', 'email_url', 'tenant']) {
         if (applyDefaultValues) {
-          $(`#oauth2_${custom}`).val($(`#${provider}_${custom}`).val());
+          document.getElementById(`oauth2_${custom}`).value = document.getElementById(`${provider}_${custom}`).value;
         }
-        if ($(`#${provider}_${custom}`).data('available')) {
-          $(`.oauth2_${custom} input`).attr('required', 'required');
-          showElem($(`.oauth2_${custom}`));
+        const customInput = document.getElementById(`${provider}_${custom}`);
+        if (customInput && customInput.getAttribute('data-available')) {
+          for (const input of document.querySelectorAll(`.oauth2_${custom} input`)) {
+            input.setAttribute('required', 'required');
+          }
+          showElem(`.oauth2_${custom}`);
         }
       }
     }
   }
 
   function onEnableLdapGroupsChange() {
-    toggleElem($('#ldap-group-options'), $('.js-ldap-group-toggle').is(':checked'));
+    toggleElem(document.getElementById('ldap-group-options'), $('.js-ldap-group-toggle')[0].checked);
   }
 
   // New authentication
-  if ($('.admin.new.authentication').length > 0) {
-    $('#auth_type').on('change', function () {
-      hideElem($('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi'));
+  if (document.querySelector('.admin.new.authentication')) {
+    document.getElementById('auth_type')?.addEventListener('change', function () {
+      hideElem('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi');
 
-      $('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]').removeAttr('required');
-      $('.binddnrequired').removeClass('required');
+      for (const input of document.querySelectorAll('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]')) {
+        input.removeAttribute('required');
+      }
 
-      const authType = $(this).val();
+      document.querySelector('.binddnrequired')?.classList.remove('required');
+
+      const authType = this.value;
       switch (authType) {
         case '2': // LDAP
-          showElem($('.ldap'));
-          $('.binddnrequired input, .ldap div.required:not(.dldap) input').attr('required', 'required');
-          $('.binddnrequired').addClass('required');
+          showElem('.ldap');
+          for (const input of document.querySelectorAll('.binddnrequired input, .ldap div.required:not(.dldap) input')) {
+            input.setAttribute('required', 'required');
+          }
+          document.querySelector('.binddnrequired')?.classList.add('required');
           break;
         case '3': // SMTP
-          showElem($('.smtp'));
-          showElem($('.has-tls'));
-          $('.smtp div.required input, .has-tls').attr('required', 'required');
+          showElem('.smtp');
+          showElem('.has-tls');
+          for (const input of document.querySelectorAll('.smtp div.required input, .has-tls')) {
+            input.setAttribute('required', 'required');
+          }
           break;
         case '4': // PAM
-          showElem($('.pam'));
-          $('.pam input').attr('required', 'required');
+          showElem('.pam');
+          for (const input of document.querySelectorAll('.pam input')) {
+            input.setAttribute('required', 'required');
+          }
           break;
         case '5': // LDAP
-          showElem($('.dldap'));
-          $('.dldap div.required:not(.ldap) input').attr('required', 'required');
+          showElem('.dldap');
+          for (const input of document.querySelectorAll('.dldap div.required:not(.ldap) input')) {
+            input.setAttribute('required', 'required');
+          }
           break;
         case '6': // OAuth2
-          showElem($('.oauth2'));
-          $('.oauth2 div.required:not(.oauth2_use_custom_url,.oauth2_use_custom_url_field,.open_id_connect_auto_discovery_url) input').attr('required', 'required');
+          showElem('.oauth2');
+          for (const input of document.querySelectorAll('.oauth2 div.required:not(.oauth2_use_custom_url,.oauth2_use_custom_url_field,.open_id_connect_auto_discovery_url) input')) {
+            input.setAttribute('required', 'required');
+          }
           onOAuth2Change(true);
           break;
         case '7': // SSPI
-          showElem($('.sspi'));
-          $('.sspi div.required input').attr('required', 'required');
+          showElem('.sspi');
+          for (const input of document.querySelectorAll('.sspi div.required input')) {
+            input.setAttribute('required', 'required');
+          }
           break;
       }
       if (authType === '2' || authType === '5') {
@@ -147,79 +175,81 @@ export function initAdminCommon() {
       }
     });
     $('#auth_type').trigger('change');
-    $('#security_protocol').on('change', onSecurityProtocolChange);
-    $('#use_paged_search').on('change', onUsePagedSearchChange);
-    $('#oauth2_provider').on('change', () => onOAuth2Change(true));
-    $('#oauth2_use_custom_url').on('change', () => onOAuth2UseCustomURLChange(true));
+    document.getElementById('security_protocol')?.addEventListener('change', onSecurityProtocolChange);
+    document.getElementById('use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
+    document.getElementById('oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
+    document.getElementById('oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(true));
     $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange);
   }
   // Edit authentication
-  if ($('.admin.edit.authentication').length > 0) {
-    const authType = $('#auth_type').val();
+  if (document.querySelector('.admin.edit.authentication')) {
+    const authType = document.getElementById('auth_type')?.value;
     if (authType === '2' || authType === '5') {
-      $('#security_protocol').on('change', onSecurityProtocolChange);
+      document.getElementById('security_protocol')?.addEventListener('change', onSecurityProtocolChange);
       $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange);
       onEnableLdapGroupsChange();
       if (authType === '2') {
-        $('#use_paged_search').on('change', onUsePagedSearchChange);
+        document.getElementById('use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
       }
     } else if (authType === '6') {
-      $('#oauth2_provider').on('change', () => onOAuth2Change(true));
-      $('#oauth2_use_custom_url').on('change', () => onOAuth2UseCustomURLChange(false));
+      document.getElementById('oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
+      document.getElementById('oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(false));
       onOAuth2Change(false);
     }
   }
 
-  if ($('.admin.authentication').length > 0) {
+  if (document.querySelector('.admin.authentication')) {
     $('#auth_name').on('input', function () {
       // appSubUrl is either empty or is a path that starts with `/` and doesn't have a trailing slash.
-      $('#oauth2-callback-url').text(`${window.location.origin}${appSubUrl}/user/oauth2/${encodeURIComponent($(this).val())}/callback`);
+      document.getElementById('oauth2-callback-url').textContent = `${window.location.origin}${appSubUrl}/user/oauth2/${encodeURIComponent(this.value)}/callback`;
     }).trigger('input');
   }
 
   // Notice
-  if ($('.admin.notice')) {
-    const $detailModal = $('#detail-modal');
+  if (document.querySelector('.admin.notice')) {
+    const $detailModal = document.getElementById('detail-modal');
 
     // Attach view detail modals
     $('.view-detail').on('click', function () {
       $detailModal.find('.content pre').text($(this).parents('tr').find('.notice-description').text());
-      $detailModal.find('.sub.header').text($(this).parents('tr').find('relative-time').attr('title'));
+      $detailModal.find('.sub.header').text(this.closest('tr')?.querySelector('relative-time')?.getAttribute('title'));
       $detailModal.modal('show');
       return false;
     });
 
     // Select actions
-    const $checkboxes = $('.select.table .ui.checkbox');
+    const checkboxes = document.querySelectorAll('.select.table .ui.checkbox input');
+
     $('.select.action').on('click', function () {
       switch ($(this).data('action')) {
         case 'select-all':
-          $checkboxes.checkbox('check');
+          for (const checkbox of checkboxes) {
+            checkbox.checked = true;
+          }
           break;
         case 'deselect-all':
-          $checkboxes.checkbox('uncheck');
+          for (const checkbox of checkboxes) {
+            checkbox.checked = false;
+          }
           break;
         case 'inverse':
-          $checkboxes.checkbox('toggle');
+          for (const checkbox of checkboxes) {
+            checkbox.checked = !checkbox.checked;
+          }
           break;
       }
     });
-    $('#delete-selection').on('click', function (e) {
+    document.getElementById('delete-selection')?.addEventListener('click', async function (e) {
       e.preventDefault();
-      const $this = $(this);
-      $this.addClass('loading disabled');
-      const ids = [];
-      $checkboxes.each(function () {
-        if ($(this).checkbox('is checked')) {
-          ids.push($(this).data('id'));
+      this.classList.add('is-loading', 'disabled');
+      const data = new FormData();
+      for (const checkbox of checkboxes) {
+        if (checkbox.checked) {
+          data.append('ids[]', checkbox.closest('.ui.checkbox').getAttribute('data-id'));
         }
-      });
-      $.post($this.data('link'), {
-        _csrf: csrfToken,
-        ids
-      }).done(() => {
-        window.location.href = $this.data('redirect');
-      });
+      }
+      await POST(this.getAttribute('data-link'), {data});
+      window.location.href = this.getAttribute('data-redirect');
     });
   }
 }
diff --git a/web_src/js/features/admin/users.js b/web_src/js/features/admin/users.js
index c8edaab549..7cac603b5c 100644
--- a/web_src/js/features/admin/users.js
+++ b/web_src/js/features/admin/users.js
@@ -1,34 +1,39 @@
-import $ from 'jquery';
-
 export function initAdminUserListSearchForm() {
   const searchForm = window.config.pageData.adminUserListSearchForm;
   if (!searchForm) return;
 
-  const $form = $('#user-list-search-form');
-  if (!$form.length) return;
+  const form = document.querySelector('#user-list-search-form');
+  if (!form) return;
 
-  $form.find(`button[name=sort][value=${searchForm.SortType}]`).addClass('active');
+  for (const button of form.querySelectorAll(`button[name=sort][value="${searchForm.SortType}"]`)) {
+    button.classList.add('active');
+  }
 
   if (searchForm.StatusFilterMap) {
     for (const [k, v] of Object.entries(searchForm.StatusFilterMap)) {
       if (!v) continue;
-      $form.find(`input[name="status_filter[${k}]"][value=${v}]`).prop('checked', true);
+      for (const input of form.querySelectorAll(`input[name="status_filter[${k}]"][value="${v}"]`)) {
+        input.checked = true;
+      }
     }
   }
 
-  $form.find(`input[type=radio]`).on('click', () => {
-    $form.trigger('submit');
-    return false;
-  });
-
-  $form.find('.j-reset-status-filter').on('click', () => {
-    $form.find(`input[type=radio]`).each((_, e) => {
-      const $e = $(e);
-      if ($e.attr('name').startsWith('status_filter[')) {
-        $e.prop('checked', false);
-      }
+  for (const radio of form.querySelectorAll('input[type=radio]')) {
+    radio.addEventListener('click', () => {
+      form.submit();
     });
-    $form.trigger('submit');
-    return false;
-  });
+  }
+
+  const resetButtons = form.querySelectorAll('.j-reset-status-filter');
+  for (const button of resetButtons) {
+    button.addEventListener('click', (e) => {
+      e.preventDefault();
+      for (const input of form.querySelectorAll('input[type=radio]')) {
+        if (input.name.startsWith('status_filter[')) {
+          input.checked = false;
+        }
+      }
+      form.submit();
+    });
+  }
 }
diff --git a/web_src/js/features/autofocus-end.js b/web_src/js/features/autofocus-end.js
new file mode 100644
index 0000000000..da71ce9536
--- /dev/null
+++ b/web_src/js/features/autofocus-end.js
@@ -0,0 +1,6 @@
+export function initAutoFocusEnd() {
+  for (const el of document.querySelectorAll('.js-autofocus-end')) {
+    el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus.
+    el.setSelectionRange(el.value.length, el.value.length);
+  }
+}
diff --git a/web_src/js/features/captcha.js b/web_src/js/features/captcha.js
index 3da5dbda41..c803a5006b 100644
--- a/web_src/js/features/captcha.js
+++ b/web_src/js/features/captcha.js
@@ -9,7 +9,7 @@ export async function initCaptcha() {
 
   const params = {
     sitekey: siteKey,
-    theme: isDark ? 'dark' : 'light'
+    theme: isDark ? 'dark' : 'light',
   };
 
   switch (captchaEl.getAttribute('data-captcha-type')) {
@@ -42,7 +42,7 @@ export async function initCaptcha() {
         siteKey: {
           instanceUrl: new URL(instanceURL),
           key: siteKey,
-        }
+        },
       });
       break;
     }
diff --git a/web_src/js/features/citation.js b/web_src/js/features/citation.js
index 61f378f0f2..918a467136 100644
--- a/web_src/js/features/citation.js
+++ b/web_src/js/features/citation.js
@@ -1,8 +1,9 @@
 import $ from 'jquery';
+import {getCurrentLocale} from '../utils.js';
 
 const {pageData} = window.config;
 
-async function initInputCitationValue($citationCopyApa, $citationCopyBibtex) {
+async function initInputCitationValue(citationCopyApa, citationCopyBibtex) {
   const [{Cite, plugins}] = await Promise.all([
     import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'),
     import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'),
@@ -14,11 +15,11 @@ async function initInputCitationValue($citationCopyApa, $citationCopyBibtex) {
   config.constants.fieldTypes.doi = ['field', 'literal'];
   config.constants.fieldTypes.version = ['field', 'literal'];
   const citationFormatter = new Cite(citationFileContent);
-  const lang = document.documentElement.lang || 'en-US';
+  const lang = getCurrentLocale() || 'en-US';
   const apaOutput = citationFormatter.format('bibliography', {template: 'apa', lang});
   const bibtexOutput = citationFormatter.format('bibtex', {lang});
-  $citationCopyBibtex.attr('data-text', bibtexOutput);
-  $citationCopyApa.attr('data-text', apaOutput);
+  citationCopyBibtex.setAttribute('data-text', bibtexOutput);
+  citationCopyApa.setAttribute('data-text', apaOutput);
 }
 
 export async function initCitationFileCopyContent() {
@@ -26,42 +27,50 @@ export async function initCitationFileCopyContent() {
 
   if (!pageData.citationFileContent) return;
 
-  const $citationCopyApa = $('#citation-copy-apa');
-  const $citationCopyBibtex = $('#citation-copy-bibtex');
-  const $inputContent = $('#citation-copy-content');
+  const citationCopyApa = document.getElementById('citation-copy-apa');
+  const citationCopyBibtex = document.getElementById('citation-copy-bibtex');
+  const inputContent = document.getElementById('citation-copy-content');
+
+  if ((!citationCopyApa && !citationCopyBibtex) || !inputContent) return;
 
-  if ((!$citationCopyApa.length && !$citationCopyBibtex.length) || !$inputContent.length) return;
   const updateUi = () => {
     const isBibtex = (localStorage.getItem('citation-copy-format') || defaultCitationFormat) === 'bibtex';
-    const copyContent = (isBibtex ? $citationCopyBibtex : $citationCopyApa).attr('data-text');
-
-    $inputContent.val(copyContent);
-    $citationCopyBibtex.toggleClass('primary', isBibtex);
-    $citationCopyApa.toggleClass('primary', !isBibtex);
+    const copyContent = (isBibtex ? citationCopyBibtex : citationCopyApa).getAttribute('data-text');
+    inputContent.value = copyContent;
+    citationCopyBibtex.classList.toggle('primary', isBibtex);
+    citationCopyApa.classList.toggle('primary', !isBibtex);
   };
 
-  try {
-    await initInputCitationValue($citationCopyApa, $citationCopyBibtex);
-  } catch (e) {
-    console.error(`initCitationFileCopyContent error: ${e}`, e);
-    return;
-  }
-  updateUi();
+  document.getElementById('cite-repo-button')?.addEventListener('click', async (e) => {
+    const dropdownBtn = e.target.closest('.ui.dropdown.button');
+    dropdownBtn.classList.add('is-loading');
 
-  $citationCopyApa.on('click', () => {
-    localStorage.setItem('citation-copy-format', 'apa');
-    updateUi();
-  });
-  $citationCopyBibtex.on('click', () => {
-    localStorage.setItem('citation-copy-format', 'bibtex');
-    updateUi();
-  });
+    try {
+      try {
+        await initInputCitationValue(citationCopyApa, citationCopyBibtex);
+      } catch (e) {
+        console.error(`initCitationFileCopyContent error: ${e}`, e);
+        return;
+      }
+      updateUi();
 
-  $inputContent.on('click', () => {
-    $inputContent.trigger('select');
-  });
+      citationCopyApa.addEventListener('click', () => {
+        localStorage.setItem('citation-copy-format', 'apa');
+        updateUi();
+      });
+
+      citationCopyBibtex.addEventListener('click', () => {
+        localStorage.setItem('citation-copy-format', 'bibtex');
+        updateUi();
+      });
+
+      inputContent.addEventListener('click', () => {
+        inputContent.select();
+      });
+    } finally {
+      dropdownBtn.classList.remove('is-loading');
+    }
 
-  $('#cite-repo-button').on('click', () => {
     $('#cite-repo-modal').modal('show');
   });
 }
diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js
index 224628658e..daf7e2ae2d 100644
--- a/web_src/js/features/clipboard.js
+++ b/web_src/js/features/clipboard.js
@@ -5,37 +5,28 @@ import {clippie} from 'clippie';
 const {copy_success, copy_error} = window.config.i18n;
 
 // Enable clipboard copy from HTML attributes. These properties are supported:
-// - data-clipboard-text: Direct text to copy, has highest precedence
+// - data-clipboard-text: Direct text to copy
 // - data-clipboard-target: Holds a selector for a <input> or <textarea> whose content is copied
 // - data-clipboard-text-type: When set to 'url' will convert relative to absolute urls
 export function initGlobalCopyToClipboardListener() {
-  document.addEventListener('click', (e) => {
-    let target = e.target;
-    // In case <button data-clipboard-text><svg></button>, so we just search
-    // up to 3 levels for performance
-    for (let i = 0; i < 3 && target; i++) {
-      let text = target.getAttribute('data-clipboard-text');
+  document.addEventListener('click', async (e) => {
+    const target = e.target.closest('[data-clipboard-text], [data-clipboard-target]');
+    if (!target) return;
 
-      if (!text && target.getAttribute('data-clipboard-target')) {
-        text = document.querySelector(target.getAttribute('data-clipboard-target'))?.value;
-      }
+    e.preventDefault();
 
-      if (text && target.getAttribute('data-clipboard-text-type') === 'url') {
-        text = toAbsoluteUrl(text);
-      }
+    let text = target.getAttribute('data-clipboard-text');
+    if (!text) {
+      text = document.querySelector(target.getAttribute('data-clipboard-target'))?.value;
+    }
 
-      if (text) {
-        e.preventDefault();
+    if (text && target.getAttribute('data-clipboard-text-type') === 'url') {
+      text = toAbsoluteUrl(text);
+    }
 
-        (async() => {
-          const success = await clippie(text);
-          showTemporaryTooltip(target, success ? copy_success : copy_error);
-        })();
-
-        break;
-      }
-
-      target = target.parentElement;
+    if (text) {
+      const success = await clippie(text);
+      showTemporaryTooltip(target, success ? copy_success : copy_error);
     }
   });
 }
diff --git a/web_src/js/features/code-frequency.js b/web_src/js/features/code-frequency.js
new file mode 100644
index 0000000000..47e1539ddc
--- /dev/null
+++ b/web_src/js/features/code-frequency.js
@@ -0,0 +1,21 @@
+import {createApp} from 'vue';
+
+export async function initRepoCodeFrequency() {
+  const el = document.getElementById('repo-code-frequency-chart');
+  if (!el) return;
+
+  const {default: RepoCodeFrequency} = await import(/* webpackChunkName: "code-frequency-graph" */'../components/RepoCodeFrequency.vue');
+  try {
+    const View = createApp(RepoCodeFrequency, {
+      locale: {
+        loadingTitle: el.getAttribute('data-locale-loading-title'),
+        loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
+        loadingInfo: el.getAttribute('data-locale-loading-info'),
+      },
+    });
+    View.mount(el);
+  } catch (err) {
+    console.error('RepoCodeFrequency failed to load', err);
+    el.textContent = el.getAttribute('data-locale-component-failed-to-load');
+  }
+}
diff --git a/web_src/js/features/codeeditor.js b/web_src/js/features/codeeditor.js
index fceb2f7620..4fb8bb9e63 100644
--- a/web_src/js/features/codeeditor.js
+++ b/web_src/js/features/codeeditor.js
@@ -80,7 +80,7 @@ export async function createMonaco(textarea, filename, editorOpts) {
     rules: [
       {
         background: getColor('--color-code-bg'),
-      }
+      },
     ],
     colors: {
       'editor.background': getColor('--color-code-bg'),
@@ -98,7 +98,7 @@ export async function createMonaco(textarea, filename, editorOpts) {
       'input.foreground': getColor('--color-input-text'),
       'scrollbar.shadow': getColor('--color-shadow'),
       'progressBar.background': getColor('--color-primary'),
-    }
+    },
   });
 
   // Quick fix: https://github.com/microsoft/monaco-editor/issues/2962
diff --git a/web_src/js/features/colorpicker.js b/web_src/js/features/colorpicker.js
index a5fdb3f5a6..6d00d908c9 100644
--- a/web_src/js/features/colorpicker.js
+++ b/web_src/js/features/colorpicker.js
@@ -1,10 +1,66 @@
-export async function createColorPicker($els) {
-  if (!$els || !$els.length) return;
+import {createTippy} from '../modules/tippy.js';
+
+export async function initColorPickers() {
+  const els = document.getElementsByClassName('js-color-picker-input');
+  if (!els.length) return;
 
   await Promise.all([
-    import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors'),
-    import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors/jquery.minicolors.css'),
+    import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'),
+    import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
   ]);
 
-  $els.minicolors();
+  for (const el of els) {
+    initPicker(el);
+  }
+}
+
+function updateSquare(el, newValue) {
+  el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent';
+}
+
+function updatePicker(el, newValue) {
+  el.setAttribute('color', newValue);
+}
+
+function initPicker(el) {
+  const input = el.querySelector('input');
+
+  const square = document.createElement('div');
+  square.classList.add('preview-square');
+  updateSquare(square, input.value);
+  el.append(square);
+
+  const picker = document.createElement('hex-color-picker');
+  picker.addEventListener('color-changed', (e) => {
+    input.value = e.detail.value;
+    input.focus();
+    updateSquare(square, e.detail.value);
+  });
+
+  input.addEventListener('input', (e) => {
+    updateSquare(square, e.target.value);
+    updatePicker(picker, e.target.value);
+  });
+
+  createTippy(input, {
+    trigger: 'focus click',
+    theme: 'bare',
+    hideOnClick: true,
+    content: picker,
+    placement: 'bottom-start',
+    interactive: true,
+    onShow() {
+      updatePicker(picker, input.value);
+    },
+  });
+
+  // init precolors
+  for (const colorEl of el.querySelectorAll('.precolors .color')) {
+    colorEl.addEventListener('click', (e) => {
+      const newValue = e.target.getAttribute('data-color-hex');
+      input.value = newValue;
+      input.dispatchEvent(new Event('input', {bubbles: true}));
+      updateSquare(square, newValue);
+    });
+  }
 }
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index e8b546970f..e7db9b2336 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -2,7 +2,6 @@ import $ from 'jquery';
 import '../vendor/jquery.are-you-sure.js';
 import {clippie} from 'clippie';
 import {createDropzone} from './dropzone.js';
-import {initCompColorPicker} from './comp/ColorPicker.js';
 import {showGlobalErrorMessage} from '../bootstrap.js';
 import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
 import {svg} from '../svg.js';
@@ -11,7 +10,7 @@ import {htmlEscape} from 'escape-goat';
 import {showTemporaryTooltip} from '../modules/tippy.js';
 import {confirmModal} from './comp/ConfirmModal.js';
 import {showErrorToast} from '../modules/toast.js';
-import {request, POST} from '../modules/fetch.js';
+import {request, POST, GET} from '../modules/fetch.js';
 import '../htmx.js';
 
 const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
@@ -19,7 +18,7 @@ const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
 export function initGlobalFormDirtyLeaveConfirm() {
   // Warn users that try to leave a page after entering data into a form.
   // Except on sign-in pages, and for forms marked as 'ignore-dirty'.
-  if ($('.user.signin').length === 0) {
+  if (!$('.user.signin').length) {
     $('form:not(.ignore-dirty)').areYouSure();
   }
 }
@@ -37,11 +36,10 @@ export function initHeadNavbarContentToggle() {
 }
 
 export function initFootLanguageMenu() {
-  function linkLanguageAction() {
+  async function linkLanguageAction() {
     const $this = $(this);
-    $.get($this.data('url')).always(() => {
-      window.location.reload();
-    });
+    await GET($this.data('url'));
+    window.location.reload();
   }
 
   $('.language-menu a[lang]').on('click', linkLanguageAction);
@@ -92,19 +90,26 @@ async function fetchActionDoRequest(actionElem, url, opt) {
       } else {
         window.location.reload();
       }
+      return;
     } else if (resp.status >= 400 && resp.status < 500) {
       const data = await resp.json();
       // the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
       // but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
-      showErrorToast(data.errorMessage || `server error: ${resp.status}`);
+      if (data.errorMessage) {
+        showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'});
+      } else {
+        showErrorToast(`server error: ${resp.status}`);
+      }
     } else {
       showErrorToast(`server error: ${resp.status}`);
     }
   } catch (e) {
-    console.error('error when doRequest', e);
-    actionElem.classList.remove('is-loading', 'small-loading-icon');
-    showErrorToast(i18n.network_error);
+    if (e.name !== 'AbortError') {
+      console.error('error when doRequest', e);
+      showErrorToast(`${i18n.network_error} ${e}`);
+    }
   }
+  actionElem.classList.remove('is-loading', 'loading-icon-2px');
 }
 
 async function formFetchAction(e) {
@@ -116,7 +121,7 @@ async function formFetchAction(e) {
 
   formEl.classList.add('is-loading');
   if (formEl.clientHeight < 50) {
-    formEl.classList.add('small-loading-icon');
+    formEl.classList.add('loading-icon-2px');
   }
 
   const formMethod = formEl.getAttribute('method') || 'get';
@@ -190,8 +195,6 @@ export function initGlobalCommon() {
   $uiDropdowns.filter('.upward').dropdown('setting', 'direction', 'upward');
   $uiDropdowns.filter('.downward').dropdown('setting', 'direction', 'downward');
 
-  $('.ui.checkbox').checkbox();
-
   $('.tabular.menu .item').tab();
 
   initSubmitEventPolyfill();
@@ -200,65 +203,68 @@ export function initGlobalCommon() {
 }
 
 export function initGlobalDropzone() {
-  // Dropzone
   for (const el of document.querySelectorAll('.dropzone')) {
-    const $dropzone = $(el);
-    const _promise = createDropzone(el, {
-      url: $dropzone.data('upload-url'),
-      headers: {'X-Csrf-Token': csrfToken},
-      maxFiles: $dropzone.data('max-file'),
-      maxFilesize: $dropzone.data('max-size'),
-      acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
-      addRemoveLinks: true,
-      dictDefaultMessage: $dropzone.data('default-message'),
-      dictInvalidFileType: $dropzone.data('invalid-input-type'),
-      dictFileTooBig: $dropzone.data('file-too-big'),
-      dictRemoveFile: $dropzone.data('remove-file'),
-      timeout: 0,
-      thumbnailMethod: 'contain',
-      thumbnailWidth: 480,
-      thumbnailHeight: 480,
-      init() {
-        this.on('success', (file, data) => {
-          file.uuid = data.uuid;
-          const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
-          $dropzone.find('.files').append(input);
-          // Create a "Copy Link" element, to conveniently copy the image
-          // or file link as Markdown to the clipboard
-          const copyLinkElement = document.createElement('div');
-          copyLinkElement.className = 'gt-text-center';
-          // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone
-          copyLinkElement.innerHTML = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`;
-          copyLinkElement.addEventListener('click', async (e) => {
-            e.preventDefault();
-            let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`;
-            if (file.type.startsWith('image/')) {
-              fileMarkdown = `!${fileMarkdown}`;
-            } else if (file.type.startsWith('video/')) {
-              fileMarkdown = `<video src="/attachments/${file.uuid}" title="${htmlEscape(file.name)}" controls></video>`;
-            }
-            const success = await clippie(fileMarkdown);
-            showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error);
-          });
-          file.previewTemplate.append(copyLinkElement);
-        });
-        this.on('removedfile', (file) => {
-          $(`#${file.uuid}`).remove();
-          if ($dropzone.data('remove-url')) {
-            POST($dropzone.data('remove-url'), {
-              data: new URLSearchParams({file: file.uuid}),
-            });
-          }
-        });
-        this.on('error', function (file, message) {
-          showErrorToast(message);
-          this.removeFile(file);
-        });
-      },
-    });
+    initDropzone(el);
   }
 }
 
+export function initDropzone(el) {
+  const $dropzone = $(el);
+  const _promise = createDropzone(el, {
+    url: $dropzone.data('upload-url'),
+    headers: {'X-Csrf-Token': csrfToken},
+    maxFiles: $dropzone.data('max-file'),
+    maxFilesize: $dropzone.data('max-size'),
+    acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
+    addRemoveLinks: true,
+    dictDefaultMessage: $dropzone.data('default-message'),
+    dictInvalidFileType: $dropzone.data('invalid-input-type'),
+    dictFileTooBig: $dropzone.data('file-too-big'),
+    dictRemoveFile: $dropzone.data('remove-file'),
+    timeout: 0,
+    thumbnailMethod: 'contain',
+    thumbnailWidth: 480,
+    thumbnailHeight: 480,
+    init() {
+      this.on('success', (file, data) => {
+        file.uuid = data.uuid;
+        const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
+        $dropzone.find('.files').append($input);
+        // Create a "Copy Link" element, to conveniently copy the image
+        // or file link as Markdown to the clipboard
+        const copyLinkElement = document.createElement('div');
+        copyLinkElement.className = 'tw-text-center';
+        // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone
+        copyLinkElement.innerHTML = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`;
+        copyLinkElement.addEventListener('click', async (e) => {
+          e.preventDefault();
+          let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`;
+          if (file.type.startsWith('image/')) {
+            fileMarkdown = `!${fileMarkdown}`;
+          } else if (file.type.startsWith('video/')) {
+            fileMarkdown = `<video src="/attachments/${file.uuid}" title="${htmlEscape(file.name)}" controls></video>`;
+          }
+          const success = await clippie(fileMarkdown);
+          showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error);
+        });
+        file.previewTemplate.append(copyLinkElement);
+      });
+      this.on('removedfile', (file) => {
+        $(`#${file.uuid}`).remove();
+        if ($dropzone.data('remove-url')) {
+          POST($dropzone.data('remove-url'), {
+            data: new URLSearchParams({file: file.uuid}),
+          });
+        }
+      });
+      this.on('error', function (file, message) {
+        showErrorToast(message);
+        this.removeFile(file);
+      });
+    },
+  });
+}
+
 async function linkAction(e) {
   // A "link-action" can post AJAX request to its "data-url"
   // Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading.
@@ -292,42 +298,41 @@ export function initGlobalLinkActions() {
     const $this = $(this);
     const dataArray = $this.data();
     let filter = '';
-    if ($this.attr('data-modal-id')) {
-      filter += `#${$this.attr('data-modal-id')}`;
+    if (this.getAttribute('data-modal-id')) {
+      filter += `#${this.getAttribute('data-modal-id')}`;
     }
 
-    const dialog = $(`.delete.modal${filter}`);
-    dialog.find('.name').text($this.data('name'));
+    const $dialog = $(`.delete.modal${filter}`);
+    $dialog.find('.name').text($this.data('name'));
     for (const [key, value] of Object.entries(dataArray)) {
       if (key && key.startsWith('data')) {
-        dialog.find(`.${key}`).text(value);
+        $dialog.find(`.${key}`).text(value);
       }
     }
 
-    dialog.modal({
+    $dialog.modal({
       closable: false,
-      onApprove() {
+      onApprove: async () => {
         if ($this.data('type') === 'form') {
           $($this.data('form')).trigger('submit');
           return;
         }
-
-        const postData = {
-          _csrf: csrfToken,
-        };
+        const postData = new FormData();
         for (const [key, value] of Object.entries(dataArray)) {
           if (key && key.startsWith('data')) {
-            postData[key.slice(4)] = value;
+            postData.append(key.slice(4), value);
           }
           if (key === 'id') {
-            postData['id'] = value;
+            postData.append('id', value);
           }
         }
 
-        $.post($this.data('url'), postData).done((data) => {
+        const response = await POST($this.data('url'), {data: postData});
+        if (response.ok) {
+          const data = await response.json();
           window.location.href = data.redirect;
-        });
-      }
+        }
+      },
     }).modal('show');
   }
 
@@ -344,8 +349,7 @@ function initGlobalShowModal() {
   // If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
   $('.show-modal').on('click', function (e) {
     e.preventDefault();
-    const $el = $(this);
-    const modalSelector = $el.attr('data-modal');
+    const modalSelector = this.getAttribute('data-modal');
     const $modal = $(modalSelector);
     if (!$modal.length) {
       throw new Error('no modal for this action');
@@ -366,16 +370,13 @@ function initGlobalShowModal() {
 
       if (attrTargetAttr) {
         $attrTarget[0][attrTargetAttr] = attrib.value;
-      } else if ($attrTarget.is('input') || $attrTarget.is('textarea')) {
+      } else if ($attrTarget[0].matches('input, textarea')) {
         $attrTarget.val(attrib.value); // FIXME: add more supports like checkbox
       } else {
         $attrTarget.text(attrib.value); // FIXME: it should be more strict here, only handle div/span/p
       }
     }
-    const colorPickers = $modal.find('.color-picker');
-    if (colorPickers.length > 0) {
-      initCompColorPicker(); // FIXME: this might cause duplicate init
-    }
+
     $modal.modal('setting', {
       onApprove: () => {
         // "form-fetch-action" can handle network errors gracefully,
@@ -398,7 +399,7 @@ export function initGlobalButtons() {
     // a '.show-panel' element can show a panel, by `data-panel="selector"`
     // if it has "toggle" class, it toggles the panel
     e.preventDefault();
-    const sel = $(this).attr('data-panel');
+    const sel = this.getAttribute('data-panel');
     if (this.classList.contains('toggle')) {
       toggleElem(sel);
     } else {
@@ -409,12 +410,12 @@ export function initGlobalButtons() {
   $('.hide-panel').on('click', function (e) {
     // a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"`
     e.preventDefault();
-    let sel = $(this).attr('data-panel');
+    let sel = this.getAttribute('data-panel');
     if (sel) {
       hideElem($(sel));
       return;
     }
-    sel = $(this).attr('data-panel-closest');
+    sel = this.getAttribute('data-panel-closest');
     if (sel) {
       hideElem($(this).closest(sel));
       return;
diff --git a/web_src/js/features/common-issue-list.js b/web_src/js/features/common-issue-list.js
index 317c11219b..0c0f6c563d 100644
--- a/web_src/js/features/common-issue-list.js
+++ b/web_src/js/features/common-issue-list.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
 import {isElemHidden, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.js';
 import {GET} from '../modules/fetch.js';
 
@@ -30,42 +29,40 @@ export function parseIssueListQuickGotoLink(repoLink, searchText) {
 }
 
 export function initCommonIssueListQuickGoto() {
-  const $goto = $('#issue-list-quick-goto');
-  if (!$goto.length) return;
+  const goto = document.getElementById('issue-list-quick-goto');
+  if (!goto) return;
 
-  const $form = $goto.closest('form');
-  const $input = $form.find('input[name=q]');
-  const repoLink = $goto.attr('data-repo-link');
+  const form = goto.closest('form');
+  const input = form.querySelector('input[name=q]');
+  const repoLink = goto.getAttribute('data-repo-link');
 
-  $form.on('submit', (e) => {
+  form.addEventListener('submit', (e) => {
     // if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly
-    let doQuickGoto = !isElemHidden($goto);
-    const submitter = submitEventSubmitter(e.originalEvent);
-    if (submitter !== $form[0] && submitter !== $input[0] && submitter !== $goto[0]) doQuickGoto = false;
+    let doQuickGoto = !isElemHidden(goto);
+    const submitter = submitEventSubmitter(e);
+    if (submitter !== form && submitter !== input && submitter !== goto) doQuickGoto = false;
     if (!doQuickGoto) return;
 
     // if there is a goto button, use its link
     e.preventDefault();
-    window.location.href = $goto.attr('data-issue-goto-link');
+    window.location.href = goto.getAttribute('data-issue-goto-link');
   });
 
   const onInput = async () => {
-    const searchText = $input.val();
-
+    const searchText = input.value;
     // try to check whether the parsed goto link is valid
     let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText);
     if (targetUrl) {
       const res = await GET(`${targetUrl}/info`);
       if (res.status !== 200) targetUrl = '';
     }
-
     // if the input value has changed, then ignore the result
-    if ($input.val() !== searchText) return;
+    if (input.value !== searchText) return;
 
-    toggleElem($goto, Boolean(targetUrl));
-    $goto.attr('data-issue-goto-link', targetUrl);
+    toggleElem(goto, Boolean(targetUrl));
+    goto.setAttribute('data-issue-goto-link', targetUrl);
   };
 
-  $input.on('input', onInputDebounce(onInput));
+  input.addEventListener('input', onInputDebounce(onInput));
   onInput();
 }
diff --git a/web_src/js/features/common-organization.js b/web_src/js/features/common-organization.js
index 352e824b05..442714a3d6 100644
--- a/web_src/js/features/common-organization.js
+++ b/web_src/js/features/common-organization.js
@@ -1,14 +1,13 @@
-import $ from 'jquery';
 import {initCompLabelEdit} from './comp/LabelEdit.js';
 import {toggleElem} from '../utils/dom.js';
 
 export function initCommonOrganization() {
-  if ($('.organization').length === 0) {
+  if (!document.querySelectorAll('.organization').length) {
     return;
   }
 
-  $('.organization.settings.options #org_name').on('input', function () {
-    const nameChanged = $(this).val().toLowerCase() !== $(this).attr('data-org-name').toLowerCase();
+  document.querySelector('.organization.settings.options #org_name')?.addEventListener('input', function () {
+    const nameChanged = this.value.toLowerCase() !== this.getAttribute('data-org-name').toLowerCase();
     toggleElem('#org-name-change-prompt', nameChanged);
   });
 
diff --git a/web_src/js/features/comp/ColorPicker.js b/web_src/js/features/comp/ColorPicker.js
deleted file mode 100644
index 5665b7a24a..0000000000
--- a/web_src/js/features/comp/ColorPicker.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import $ from 'jquery';
-import {createColorPicker} from '../colorpicker.js';
-
-export function initCompColorPicker() {
-  createColorPicker($('.color-picker'));
-
-  $('.precolors .color').on('click', function () {
-    const color_hex = $(this).data('color-hex');
-    $('.color-picker').val(color_hex);
-    $('.minicolors-swatch-color').css('background-color', color_hex);
-  });
-}
diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
index d486c5830a..d3fab375a9 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.js
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -2,29 +2,30 @@ import '@github/markdown-toolbar-element';
 import '@github/text-expander-element';
 import $ from 'jquery';
 import {attachTribute} from '../tribute.js';
-import {hideElem, showElem, autosize} from '../../utils/dom.js';
-import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
+import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.js';
+import {initEasyMDEPaste, initTextareaPaste} from './Paste.js';
 import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
 import {renderPreviewPanelContent} from '../repo-editor.js';
 import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js';
 import {initTextExpander} from './TextExpander.js';
 import {showErrorToast} from '../../modules/toast.js';
+import {POST} from '../../modules/fetch.js';
 
 let elementIdCounter = 0;
 
 /**
  * validate if the given textarea is non-empty.
- * @param {jQuery} $textarea
+ * @param {HTMLElement} textarea - The textarea element to be validated.
  * @returns {boolean} returns true if validation succeeded.
  */
-export function validateTextareaNonEmpty($textarea) {
+export function validateTextareaNonEmpty(textarea) {
   // When using EasyMDE, the original edit area HTML element is hidden, breaking HTML5 input validation.
   // The workaround (https://github.com/sparksuite/simplemde-markdown-editor/issues/324) doesn't work with contenteditable, so we just show an alert.
-  if (!$textarea.val()) {
-    if ($textarea.is(':visible')) {
-      $textarea.prop('required', true);
-      const $form = $textarea.parents('form');
-      $form[0]?.reportValidity();
+  if (!textarea.value) {
+    if (isElemVisible(textarea)) {
+      textarea.required = true;
+      const form = textarea.closest('form');
+      form?.reportValidity();
     } else {
       // The alert won't hurt users too much, because we are dropping the EasyMDE and the check only occurs in a few places.
       showErrorToast('Require non-empty content');
@@ -83,6 +84,17 @@ class ComboMarkdownEditor {
       if (el.nodeName === 'BUTTON' && !el.getAttribute('type')) el.setAttribute('type', 'button');
     }
 
+    this.textarea.addEventListener('keydown', (e) => {
+      if (e.shiftKey) {
+        e.target._shiftDown = true;
+      }
+    });
+    this.textarea.addEventListener('keyup', (e) => {
+      if (!e.shiftKey) {
+        e.target._shiftDown = false;
+      }
+    });
+
     const monospaceButton = this.container.querySelector('.markdown-switch-monospace');
     const monospaceEnabled = localStorage?.getItem('markdown-editor-monospace') === 'true';
     const monospaceText = monospaceButton.getAttribute(monospaceEnabled ? 'data-disable-text' : 'data-enable-text');
@@ -93,7 +105,7 @@ class ComboMarkdownEditor {
       e.preventDefault();
       const enabled = localStorage?.getItem('markdown-editor-monospace') !== 'true';
       localStorage.setItem('markdown-editor-monospace', String(enabled));
-      this.textarea.classList.toggle('gt-mono', enabled);
+      this.textarea.classList.toggle('tw-font-mono', enabled);
       const text = monospaceButton.getAttribute(enabled ? 'data-disable-text' : 'data-enable-text');
       monospaceButton.setAttribute('data-tooltip-content', text);
       monospaceButton.setAttribute('aria-checked', String(enabled));
@@ -107,7 +119,7 @@ class ComboMarkdownEditor {
     });
 
     if (this.dropzone) {
-      initTextareaImagePaste(this.textarea, this.dropzone);
+      initTextareaPaste(this.textarea, this.dropzone);
     }
   }
 
@@ -120,43 +132,41 @@ class ComboMarkdownEditor {
 
   setupTab() {
     const $container = $(this.container);
-    const $tabMenu = $container.find('.tabular.menu');
-    const $tabs = $tabMenu.find('> .item');
+    const tabs = $container[0].querySelectorAll('.tabular.menu > .item');
 
     // Fomantic Tab requires the "data-tab" to be globally unique.
     // So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
-    const $tabEditor = $tabs.filter(`.item[data-tab-for="markdown-writer"]`);
-    const $tabPreviewer = $tabs.filter(`.item[data-tab-for="markdown-previewer"]`);
-    $tabEditor.attr('data-tab', `markdown-writer-${elementIdCounter}`);
-    $tabPreviewer.attr('data-tab', `markdown-previewer-${elementIdCounter}`);
-    const $panelEditor = $container.find('.ui.tab[data-tab-panel="markdown-writer"]');
-    const $panelPreviewer = $container.find('.ui.tab[data-tab-panel="markdown-previewer"]');
-    $panelEditor.attr('data-tab', `markdown-writer-${elementIdCounter}`);
-    $panelPreviewer.attr('data-tab', `markdown-previewer-${elementIdCounter}`);
+    const tabEditor = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-writer');
+    const tabPreviewer = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-previewer');
+    tabEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
+    tabPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
+    const panelEditor = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-writer"]');
+    const panelPreviewer = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-previewer"]');
+    panelEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
+    panelPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
     elementIdCounter++;
 
-    $tabEditor[0].addEventListener('click', () => {
+    tabEditor.addEventListener('click', () => {
       requestAnimationFrame(() => {
         this.focus();
       });
     });
 
-    $tabs.tab();
+    $(tabs).tab();
 
-    this.previewUrl = $tabPreviewer.attr('data-preview-url');
-    this.previewContext = $tabPreviewer.attr('data-preview-context');
+    this.previewUrl = tabPreviewer.getAttribute('data-preview-url');
+    this.previewContext = tabPreviewer.getAttribute('data-preview-context');
     this.previewMode = this.options.previewMode ?? 'comment';
     this.previewWiki = this.options.previewWiki ?? false;
-    $tabPreviewer.on('click', () => {
-      $.post(this.previewUrl, {
-        _csrf: window.config.csrfToken,
-        mode: this.previewMode,
-        context: this.previewContext,
-        text: this.value(),
-        wiki: this.previewWiki,
-      }, (data) => {
-        renderPreviewPanelContent($panelPreviewer, data);
-      });
+    tabPreviewer.addEventListener('click', async () => {
+      const formData = new FormData();
+      formData.append('mode', this.previewMode);
+      formData.append('context', this.previewContext);
+      formData.append('text', this.value());
+      formData.append('wiki', this.previewWiki);
+      const response = await POST(this.previewUrl, {data: formData});
+      const data = await response.text();
+      renderPreviewPanelContent($(panelPreviewer), data);
     });
   }
 
@@ -241,7 +251,7 @@ class ComboMarkdownEditor {
     });
     this.applyEditorHeights(this.container.querySelector('.CodeMirror-scroll'), this.options.editorHeights);
     await attachTribute(this.easyMDE.codemirror.getInputField(), {mentions: true, emoji: true});
-    initEasyMDEImagePaste(this.easyMDE, this.dropzone);
+    initEasyMDEPaste(this.easyMDE, this.dropzone);
     hideElem(this.textareaMarkdownToolbar);
   }
 
diff --git a/web_src/js/features/comp/EasyMDEToolbarActions.js b/web_src/js/features/comp/EasyMDEToolbarActions.js
index 8286d5d871..c97d683704 100644
--- a/web_src/js/features/comp/EasyMDEToolbarActions.js
+++ b/web_src/js/features/comp/EasyMDEToolbarActions.js
@@ -139,7 +139,7 @@ export function easyMDEToolbarActions(EasyMDE, editor) {
       },
       icon: svg('octicon-chevron-right'),
       title: 'Add Inline Code',
-    }
+    },
   };
 
   for (const [key, value] of Object.entries(actions)) {
diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js
index 2a22190e10..2cc75cc6b0 100644
--- a/web_src/js/features/comp/LabelEdit.js
+++ b/web_src/js/features/comp/LabelEdit.js
@@ -1,40 +1,43 @@
 import $ from 'jquery';
-import {initCompColorPicker} from './ColorPicker.js';
 
 function isExclusiveScopeName(name) {
   return /.*[^/]\/[^/].*/.test(name);
 }
 
 function updateExclusiveLabelEdit(form) {
-  const nameInput = $(`${form} .label-name-input`);
-  const exclusiveField = $(`${form} .label-exclusive-input-field`);
-  const exclusiveCheckbox = $(`${form} .label-exclusive-input`);
-  const exclusiveWarning = $(`${form} .label-exclusive-warning`);
+  const nameInput = document.querySelector(`${form} .label-name-input`);
+  const exclusiveField = document.querySelector(`${form} .label-exclusive-input-field`);
+  const exclusiveCheckbox = document.querySelector(`${form} .label-exclusive-input`);
+  const exclusiveWarning = document.querySelector(`${form} .label-exclusive-warning`);
 
-  if (isExclusiveScopeName(nameInput.val())) {
-    exclusiveField.removeClass('muted');
-    exclusiveField.removeAttr('aria-disabled');
-    if (exclusiveCheckbox.prop('checked') && exclusiveCheckbox.data('exclusive-warn')) {
-      exclusiveWarning.removeClass('gt-hidden');
+  if (isExclusiveScopeName(nameInput.value)) {
+    exclusiveField?.classList.remove('muted');
+    exclusiveField?.removeAttribute('aria-disabled');
+    if (exclusiveCheckbox.checked && exclusiveCheckbox.getAttribute('data-exclusive-warn')) {
+      exclusiveWarning?.classList.remove('tw-hidden');
     } else {
-      exclusiveWarning.addClass('gt-hidden');
+      exclusiveWarning?.classList.add('tw-hidden');
     }
   } else {
-    exclusiveField.addClass('muted');
-    exclusiveField.attr('aria-disabled', 'true');
-    exclusiveWarning.addClass('gt-hidden');
+    exclusiveField?.classList.add('muted');
+    exclusiveField?.setAttribute('aria-disabled', 'true');
+    exclusiveWarning?.classList.add('tw-hidden');
   }
 }
 
 export function initCompLabelEdit(selector) {
   if (!$(selector).length) return;
-  initCompColorPicker();
 
   // Create label
   $('.new-label.button').on('click', () => {
     updateExclusiveLabelEdit('.new-label');
     $('.new-label.modal').modal({
       onApprove() {
+        const form = document.querySelector('.new-label.form');
+        if (!form.checkValidity()) {
+          form.reportValidity();
+          return false;
+        }
         $('.new-label.form').trigger('submit');
       },
     }).modal('show');
@@ -43,29 +46,35 @@ export function initCompLabelEdit(selector) {
 
   // Edit label
   $('.edit-label-button').on('click', function () {
-    $('.edit-label .color-picker').minicolors('value', $(this).data('color'));
     $('#label-modal-id').val($(this).data('id'));
 
-    const nameInput = $('.edit-label .label-name-input');
-    nameInput.val($(this).data('title'));
+    const $nameInput = $('.edit-label .label-name-input');
+    $nameInput.val($(this).data('title'));
 
-    const isArchivedCheckbox = $('.edit-label .label-is-archived-input');
-    isArchivedCheckbox.prop('checked', this.hasAttribute('data-is-archived'));
+    const $isArchivedCheckbox = $('.edit-label .label-is-archived-input');
+    $isArchivedCheckbox[0].checked = this.hasAttribute('data-is-archived');
 
-    const exclusiveCheckbox = $('.edit-label .label-exclusive-input');
-    exclusiveCheckbox.prop('checked', this.hasAttribute('data-exclusive'));
+    const $exclusiveCheckbox = $('.edit-label .label-exclusive-input');
+    $exclusiveCheckbox[0].checked = this.hasAttribute('data-exclusive');
     // Warn when label was previously not exclusive and used in issues
-    exclusiveCheckbox.data('exclusive-warn',
+    $exclusiveCheckbox.data('exclusive-warn',
       $(this).data('num-issues') > 0 &&
-      (!this.hasAttribute('data-exclusive') || !isExclusiveScopeName(nameInput.val())));
+      (!this.hasAttribute('data-exclusive') || !isExclusiveScopeName($nameInput.val())));
     updateExclusiveLabelEdit('.edit-label');
 
-    $('.edit-label .label-desc-input').val($(this).data('description'));
-    $('.edit-label .color-picker').val($(this).data('color'));
-    $('.edit-label .minicolors-swatch-color').css('background-color', $(this).data('color'));
+    $('.edit-label .label-desc-input').val(this.getAttribute('data-description'));
+
+    const colorInput = document.querySelector('.edit-label .js-color-picker-input input');
+    colorInput.value = this.getAttribute('data-color');
+    colorInput.dispatchEvent(new Event('input', {bubbles: true}));
 
     $('.edit-label.modal').modal({
       onApprove() {
+        const form = document.querySelector('.edit-label.form');
+        if (!form.checkValidity()) {
+          form.reportValidity();
+          return false;
+        }
         $('.edit-label.form').trigger('submit');
       },
     }).modal('show');
diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/Paste.js
similarity index 52%
rename from web_src/js/features/comp/ImagePaste.js
rename to web_src/js/features/comp/Paste.js
index 27abcfe56f..b26296d1fc 100644
--- a/web_src/js/features/comp/ImagePaste.js
+++ b/web_src/js/features/comp/Paste.js
@@ -1,5 +1,8 @@
-import $ from 'jquery';
+import {htmlEscape} from 'escape-goat';
 import {POST} from '../../modules/fetch.js';
+import {imageInfo} from '../../utils/image.js';
+import {getPastedContent, replaceTextareaSelection} from '../../utils/dom.js';
+import {isUrl} from '../../utils/url.js';
 
 async function uploadFile(file, uploadUrl) {
   const formData = new FormData();
@@ -9,17 +12,6 @@ async function uploadFile(file, uploadUrl) {
   return await res.json();
 }
 
-function clipboardPastedImages(e) {
-  if (!e.clipboardData) return [];
-
-  const files = [];
-  for (const item of e.clipboardData.items || []) {
-    if (!item.type || !item.type.startsWith('image/')) continue;
-    files.push(item.getAsFile());
-  }
-  return files;
-}
-
 function triggerEditorContentChanged(target) {
   target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true}));
 }
@@ -90,43 +82,73 @@ class CodeMirrorEditor {
   }
 }
 
-const uploadClipboardImage = async (editor, dropzone, e) => {
-  const $dropzone = $(dropzone);
-  const uploadUrl = $dropzone.attr('data-upload-url');
-  const $files = $dropzone.find('.files');
+async function handleClipboardImages(editor, dropzone, images, e) {
+  const uploadUrl = dropzone.getAttribute('data-upload-url');
+  const filesContainer = dropzone.querySelector('.files');
 
-  if (!uploadUrl || !$files.length) return;
+  if (!dropzone || !uploadUrl || !filesContainer || !images.length) return;
 
-  const pastedImages = clipboardPastedImages(e);
-  if (!pastedImages || pastedImages.length === 0) {
-    return;
-  }
   e.preventDefault();
   e.stopPropagation();
 
-  for (const img of pastedImages) {
+  for (const img of images) {
     const name = img.name.slice(0, img.name.lastIndexOf('.'));
 
     const placeholder = `![${name}](uploading ...)`;
     editor.insertPlaceholder(placeholder);
-    const data = await uploadFile(img, uploadUrl);
-    editor.replacePlaceholder(placeholder, `![${name}](/attachments/${data.uuid})`);
 
-    const $input = $(`<input name="files" type="hidden">`).attr('id', data.uuid).val(data.uuid);
-    $files.append($input);
+    const {uuid} = await uploadFile(img, uploadUrl);
+    const {width, dppx} = await imageInfo(img);
+
+    const url = `/attachments/${uuid}`;
+    let text;
+    if (width > 0 && dppx > 1) {
+      // Scale down images from HiDPI monitors. This uses the <img> tag because it's the only
+      // method to change image size in Markdown that is supported by all implementations.
+      text = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(name)}" src="${htmlEscape(url)}">`;
+    } else {
+      text = `![${name}](${url})`;
+    }
+    editor.replacePlaceholder(placeholder, text);
+
+    const input = document.createElement('input');
+    input.setAttribute('name', 'files');
+    input.setAttribute('type', 'hidden');
+    input.setAttribute('id', uuid);
+    input.value = uuid;
+    filesContainer.append(input);
   }
-};
+}
 
-export function initEasyMDEImagePaste(easyMDE, dropzone) {
-  if (!dropzone) return;
-  easyMDE.codemirror.on('paste', async (_, e) => {
-    return uploadClipboardImage(new CodeMirrorEditor(easyMDE.codemirror), dropzone, e);
+function handleClipboardText(textarea, text, e) {
+  // when pasting links over selected text, turn it into [text](link), except when shift key is held
+  const {value, selectionStart, selectionEnd, _shiftDown} = textarea;
+  if (_shiftDown) return;
+  const selectedText = value.substring(selectionStart, selectionEnd);
+  const trimmedText = text.trim();
+  if (selectedText && isUrl(trimmedText)) {
+    e.stopPropagation();
+    e.preventDefault();
+    replaceTextareaSelection(textarea, `[${selectedText}](${trimmedText})`);
+  }
+}
+
+export function initEasyMDEPaste(easyMDE, dropzone) {
+  easyMDE.codemirror.on('paste', (_, e) => {
+    const {images} = getPastedContent(e);
+    if (images.length) {
+      handleClipboardImages(new CodeMirrorEditor(easyMDE.codemirror), dropzone, images, e);
+    }
   });
 }
 
-export function initTextareaImagePaste(textarea, dropzone) {
-  if (!dropzone) return;
-  $(textarea).on('paste', async (e) => {
-    return uploadClipboardImage(new TextareaEditor(textarea), dropzone, e.originalEvent);
+export function initTextareaPaste(textarea, dropzone) {
+  textarea.addEventListener('paste', (e) => {
+    const {images, text} = getPastedContent(e);
+    if (images.length) {
+      handleClipboardImages(new TextareaEditor(textarea), dropzone, images, e);
+    } else if (text) {
+      handleClipboardText(textarea, text, e);
+    }
   });
 }
diff --git a/web_src/js/features/comp/QuickSubmit.js b/web_src/js/features/comp/QuickSubmit.js
index 2587375a71..e6d7080bcf 100644
--- a/web_src/js/features/comp/QuickSubmit.js
+++ b/web_src/js/features/comp/QuickSubmit.js
@@ -1,5 +1,3 @@
-import $ from 'jquery';
-
 export function handleGlobalEnterQuickSubmit(target) {
   const form = target.closest('form');
   if (form) {
@@ -8,14 +6,9 @@ export function handleGlobalEnterQuickSubmit(target) {
       return;
     }
 
-    if (form.classList.contains('form-fetch-action')) {
-      form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
-      return;
-    }
-
     // here use the event to trigger the submit event (instead of calling `submit()` method directly)
     // otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
-    $(form).trigger('submit');
+    form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
   } else {
     // if no form, then the editor is for an AJAX request, dispatch an event to the target, let the target's event handler to do the AJAX request.
     // the 'ce-' prefix means this is a CustomEvent
diff --git a/web_src/js/features/comp/ReactionSelector.js b/web_src/js/features/comp/ReactionSelector.js
index 76834f8844..2def3db51a 100644
--- a/web_src/js/features/comp/ReactionSelector.js
+++ b/web_src/js/features/comp/ReactionSelector.js
@@ -5,11 +5,11 @@ export function initCompReactionSelector($parent) {
   $parent.find(`.select-reaction .item.reaction, .comment-reaction-button`).on('click', async function (e) {
     e.preventDefault();
 
-    if ($(this).hasClass('disabled')) return;
+    if (this.classList.contains('disabled')) return;
 
-    const actionUrl = $(this).closest('[data-action-url]').attr('data-action-url');
-    const reactionContent = $(this).attr('data-reaction-content');
-    const hasReacted = $(this).closest('.ui.segment.reactions').find(`a[data-reaction-content="${reactionContent}"]`).attr('data-has-reacted') === 'true';
+    const actionUrl = this.closest('[data-action-url]')?.getAttribute('data-action-url');
+    const reactionContent = this.getAttribute('data-reaction-content');
+    const hasReacted = this.closest('.ui.segment.reactions')?.querySelector(`a[data-reaction-content="${reactionContent}"]`)?.getAttribute('data-has-reacted') === 'true';
 
     const res = await POST(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, {
       data: new URLSearchParams({content: reactionContent}),
@@ -17,21 +17,21 @@ export function initCompReactionSelector($parent) {
 
     const data = await res.json();
     if (data && (data.html || data.empty)) {
-      const content = $(this).closest('.content');
-      let react = content.find('.segment.reactions');
-      if ((!data.empty || data.html === '') && react.length > 0) {
-        react.remove();
+      const $content = $(this).closest('.content');
+      let $react = $content.find('.segment.reactions');
+      if ((!data.empty || data.html === '') && $react.length > 0) {
+        $react.remove();
       }
       if (!data.empty) {
-        const attachments = content.find('.segment.bottom:first');
-        react = $(data.html);
-        if (attachments.length > 0) {
-          react.insertBefore(attachments);
+        const $attachments = $content.find('.segment.bottom:first');
+        $react = $(data.html);
+        if ($attachments.length > 0) {
+          $react.insertBefore($attachments);
         } else {
-          react.appendTo(content);
+          $react.appendTo($content);
         }
-        react.find('.dropdown').dropdown();
-        initCompReactionSelector(react);
+        $react.find('.dropdown').dropdown();
+        initCompReactionSelector($react);
       }
     }
   });
diff --git a/web_src/js/features/comp/SearchUserBox.js b/web_src/js/features/comp/SearchUserBox.js
index 992d4ef020..081c47425f 100644
--- a/web_src/js/features/comp/SearchUserBox.js
+++ b/web_src/js/features/comp/SearchUserBox.js
@@ -5,9 +5,12 @@ const {appSubUrl} = window.config;
 const looksLikeEmailAddressCheck = /^\S+@\S+$/;
 
 export function initCompSearchUserBox() {
-  const $searchUserBox = $('#search-user-box');
-  const allowEmailInput = $searchUserBox.attr('data-allow-email') === 'true';
-  const allowEmailDescription = $searchUserBox.attr('data-allow-email-description');
+  const searchUserBox = document.getElementById('search-user-box');
+  if (!searchUserBox) return;
+
+  const $searchUserBox = $(searchUserBox);
+  const allowEmailInput = searchUserBox.getAttribute('data-allow-email') === 'true';
+  const allowEmailDescription = searchUserBox.getAttribute('data-allow-email-description') ?? undefined;
   $searchUserBox.search({
     minCharacters: 2,
     apiSettings: {
@@ -19,7 +22,7 @@ export function initCompSearchUserBox() {
         $.each(response.data, (_i, item) => {
           const resultItem = {
             title: item.login,
-            image: item.avatar_url
+            image: item.avatar_url,
           };
           if (item.full_name) {
             resultItem.description = htmlEscape(item.full_name);
@@ -31,18 +34,18 @@ export function initCompSearchUserBox() {
           }
         });
 
-        if (allowEmailInput && items.length === 0 && looksLikeEmailAddressCheck.test(searchQuery)) {
+        if (allowEmailInput && !items.length && looksLikeEmailAddressCheck.test(searchQuery)) {
           const resultItem = {
             title: searchQuery,
-            description: allowEmailDescription
+            description: allowEmailDescription,
           };
           items.push(resultItem);
         }
 
         return {results: items};
-      }
+      },
     },
     searchFields: ['login', 'full_name'],
-    showNoResults: false
+    showNoResults: false,
   });
 }
diff --git a/web_src/js/features/comp/WebHookEditor.js b/web_src/js/features/comp/WebHookEditor.js
index f4c82898fd..d74b59fd2a 100644
--- a/web_src/js/features/comp/WebHookEditor.js
+++ b/web_src/js/features/comp/WebHookEditor.js
@@ -1,43 +1,44 @@
-import $ from 'jquery';
+import {POST} from '../../modules/fetch.js';
 import {hideElem, showElem, toggleElem} from '../../utils/dom.js';
 
-const {csrfToken} = window.config;
-
 export function initCompWebHookEditor() {
-  if ($('.new.webhook').length === 0) {
+  if (!document.querySelectorAll('.new.webhook').length) {
     return;
   }
 
-  $('.events.checkbox input').on('change', function () {
-    if ($(this).is(':checked')) {
-      showElem($('.events.fields'));
-    }
-  });
-  $('.non-events.checkbox input').on('change', function () {
-    if ($(this).is(':checked')) {
-      hideElem($('.events.fields'));
-    }
-  });
+  for (const input of document.querySelectorAll('.events.checkbox input')) {
+    input.addEventListener('change', function () {
+      if (this.checked) {
+        showElem('.events.fields');
+      }
+    });
+  }
 
-  const updateContentType = function () {
-    const visible = $('#http_method').val() === 'POST';
-    toggleElem($('#content_type').parent().parent(), visible);
-  };
-  updateContentType();
-  $('#http_method').on('change', () => {
+  for (const input of document.querySelectorAll('.non-events.checkbox input')) {
+    input.addEventListener('change', function () {
+      if (this.checked) {
+        hideElem('.events.fields');
+      }
+    });
+  }
+
+  // some webhooks (like Gitea) allow to set the request method (GET/POST), and it would toggle the "Content Type" field
+  const httpMethodInput = document.getElementById('http_method');
+  if (httpMethodInput) {
+    const updateContentType = function () {
+      const visible = httpMethodInput.value === 'POST';
+      toggleElem(document.getElementById('content_type').closest('.field'), visible);
+    };
     updateContentType();
-  });
+    httpMethodInput.addEventListener('change', updateContentType);
+  }
 
   // Test delivery
-  $('#test-delivery').on('click', function () {
-    const $this = $(this);
-    $this.addClass('loading disabled');
-    $.post($this.data('link'), {
-      _csrf: csrfToken
-    }).done(
-      setTimeout(() => {
-        window.location.href = $this.data('redirect');
-      }, 5000)
-    );
+  document.getElementById('test-delivery')?.addEventListener('click', async function () {
+    this.classList.add('is-loading', 'disabled');
+    await POST(this.getAttribute('data-link'));
+    setTimeout(() => {
+      window.location.href = this.getAttribute('data-redirect');
+    }, 5000);
   });
 }
diff --git a/web_src/js/features/contextpopup.js b/web_src/js/features/contextpopup.js
index 23a620b8a2..ce90f3e505 100644
--- a/web_src/js/features/contextpopup.js
+++ b/web_src/js/features/contextpopup.js
@@ -1,11 +1,10 @@
-import $ from 'jquery';
 import {createApp} from 'vue';
 import ContextPopup from '../components/ContextPopup.vue';
 import {parseIssueHref} from '../utils.js';
 import {createTippy} from '../modules/tippy.js';
 
 export function initContextPopups() {
-  const refIssues = $('.ref-issue');
+  const refIssues = document.querySelectorAll('.ref-issue');
   attachRefIssueContextPopup(refIssues);
 }
 
@@ -38,7 +37,7 @@ export function attachRefIssueContextPopup(refIssues) {
       interactiveBorder: 5,
       onShow: () => {
         el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}}));
-      }
+      },
     });
   }
 }
diff --git a/web_src/js/features/contributors.js b/web_src/js/features/contributors.js
new file mode 100644
index 0000000000..1d9cba5b9b
--- /dev/null
+++ b/web_src/js/features/contributors.js
@@ -0,0 +1,28 @@
+import {createApp} from 'vue';
+
+export async function initRepoContributors() {
+  const el = document.getElementById('repo-contributors-chart');
+  if (!el) return;
+
+  const {default: RepoContributors} = await import(/* webpackChunkName: "contributors-graph" */'../components/RepoContributors.vue');
+  try {
+    const View = createApp(RepoContributors, {
+      locale: {
+        filterLabel: el.getAttribute('data-locale-filter-label'),
+        contributionType: {
+          commits: el.getAttribute('data-locale-contribution-type-commits'),
+          additions: el.getAttribute('data-locale-contribution-type-additions'),
+          deletions: el.getAttribute('data-locale-contribution-type-deletions'),
+        },
+
+        loadingTitle: el.getAttribute('data-locale-loading-title'),
+        loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
+        loadingInfo: el.getAttribute('data-locale-loading-info'),
+      },
+    });
+    View.mount(el);
+  } catch (err) {
+    console.error('RepoContributors failed to load', err);
+    el.textContent = el.getAttribute('data-locale-component-failed-to-load');
+  }
+}
diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js
index 3d3b2a697e..03efe00701 100644
--- a/web_src/js/features/copycontent.js
+++ b/web_src/js/features/copycontent.js
@@ -19,7 +19,7 @@ export function initCopyContent() {
     // the text to copy is not in the DOM or it is an image which should be
     // fetched to copy in full resolution
     if (link) {
-      btn.classList.add('is-loading', 'small-loading-icon');
+      btn.classList.add('is-loading', 'loading-icon-2px');
       try {
         const res = await GET(link, {credentials: 'include', redirect: 'follow'});
         const contentType = res.headers.get('content-type');
@@ -33,7 +33,7 @@ export function initCopyContent() {
       } catch {
         return showTemporaryTooltip(btn, i18n.copy_error);
       } finally {
-        btn.classList.remove('is-loading', 'small-loading-icon');
+        btn.classList.remove('is-loading', 'loading-icon-2px');
       }
     } else { // text, read from DOM
       const lineEls = document.querySelectorAll('.file-view .lines-code');
diff --git a/web_src/js/features/eventsource.sharedworker.js b/web_src/js/features/eventsource.sharedworker.js
index 2ac7d93cc1..62581cf687 100644
--- a/web_src/js/features/eventsource.sharedworker.js
+++ b/web_src/js/features/eventsource.sharedworker.js
@@ -48,7 +48,7 @@ class Source {
     this.eventSource.addEventListener(eventType, (event) => {
       this.notifyClients({
         type: eventType,
-        data: event.data
+        data: event.data,
       });
     });
   }
diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js
index 80b7e83385..192a642834 100644
--- a/web_src/js/features/imagediff.js
+++ b/web_src/js/features/imagediff.js
@@ -20,19 +20,19 @@ function getDefaultSvgBoundsIfUndefined(text, src) {
     if (img.width > 1 && img.width < MaxSize && img.height > 1 && img.height < MaxSize) {
       return {
         width: img.width,
-        height: img.height
+        height: img.height,
       };
     }
     if (svg.hasAttribute('viewBox')) {
       const viewBox = svg.viewBox.baseVal;
       return {
         width: DefaultSize,
-        height: DefaultSize * viewBox.width / viewBox.height
+        height: DefaultSize * viewBox.width / viewBox.height,
       };
     }
     return {
       width: DefaultSize,
-      height: DefaultSize
+      height: DefaultSize,
     };
   }
   return null;
@@ -42,20 +42,20 @@ export function initImageDiff() {
   function createContext(image1, image2) {
     const size1 = {
       width: image1 && image1.width || 0,
-      height: image1 && image1.height || 0
+      height: image1 && image1.height || 0,
     };
     const size2 = {
       width: image2 && image2.width || 0,
-      height: image2 && image2.height || 0
+      height: image2 && image2.height || 0,
     };
     const max = {
       width: Math.max(size2.width, size1.width),
-      height: Math.max(size2.height, size1.height)
+      height: Math.max(size2.height, size1.height),
     };
 
     return {
-      image1: $(image1),
-      image2: $(image2),
+      $image1: $(image1),
+      $image2: $(image2),
       size1,
       size2,
       max,
@@ -63,14 +63,14 @@ export function initImageDiff() {
         Math.floor(max.width - size1.width) / 2,
         Math.floor(max.height - size1.height) / 2,
         Math.floor(max.width - size2.width) / 2,
-        Math.floor(max.height - size2.height) / 2
-      ]
+        Math.floor(max.height - size2.height) / 2,
+      ],
     };
   }
 
   $('.image-diff:not([data-image-diff-loaded])').each(async function() {
     const $container = $(this);
-    $container.attr('data-image-diff-loaded', 'true');
+    this.setAttribute('data-image-diff-loaded', 'true');
 
     // the container may be hidden by "viewed" checkbox, so use the parent's width for reference
     const diffContainerWidth = Math.max($container.closest('.diff-file-box').width() - 300, 100);
@@ -79,12 +79,12 @@ export function initImageDiff() {
       path: this.getAttribute('data-path-after'),
       mime: this.getAttribute('data-mime-after'),
       $images: $container.find('img.image-after'), // matches 3 <img>
-      $boundsInfo: $container.find('.bounds-info-after')
+      $boundsInfo: $container.find('.bounds-info-after'),
     }, {
       path: this.getAttribute('data-path-before'),
       mime: this.getAttribute('data-mime-before'),
       $images: $container.find('img.image-before'), // matches 3 <img>
-      $boundsInfo: $container.find('.bounds-info-before')
+      $boundsInfo: $container.find('.bounds-info-before'),
     }];
 
     await Promise.all(imageInfos.map(async (info) => {
@@ -98,8 +98,10 @@ export function initImageDiff() {
         const text = await resp.text();
         const bounds = getDefaultSvgBoundsIfUndefined(text, info.path);
         if (bounds) {
-          info.$images.attr('width', bounds.width);
-          info.$images.attr('height', bounds.height);
+          info.$images.each(function() {
+            this.setAttribute('width', bounds.width);
+            this.setAttribute('height', bounds.height);
+          });
           hideElem(info.$boundsInfo);
         }
       }
@@ -108,49 +110,61 @@ export function initImageDiff() {
     const $imagesAfter = imageInfos[0].$images;
     const $imagesBefore = imageInfos[1].$images;
 
-    initSideBySide(createContext($imagesAfter[0], $imagesBefore[0]));
+    initSideBySide(this, createContext($imagesAfter[0], $imagesBefore[0]));
     if ($imagesAfter.length > 0 && $imagesBefore.length > 0) {
       initSwipe(createContext($imagesAfter[1], $imagesBefore[1]));
       initOverlay(createContext($imagesAfter[2], $imagesBefore[2]));
     }
 
-    $container.find('> .image-diff-tabs').removeClass('is-loading');
+    this.querySelector(':scope > .image-diff-tabs')?.classList.remove('is-loading');
 
-    function initSideBySide(sizes) {
+    function initSideBySide(container, sizes) {
       let factor = 1;
       if (sizes.max.width > (diffContainerWidth - 24) / 2) {
         factor = (diffContainerWidth - 24) / 2 / sizes.max.width;
       }
 
-      const widthChanged = sizes.image1.length !== 0 && sizes.image2.length !== 0 && sizes.image1[0].naturalWidth !== sizes.image2[0].naturalWidth;
-      const heightChanged = sizes.image1.length !== 0 && sizes.image2.length !== 0 && sizes.image1[0].naturalHeight !== sizes.image2[0].naturalHeight;
-      if (sizes.image1.length !== 0) {
-        $container.find('.bounds-info-after .bounds-info-width').text(`${sizes.image1[0].naturalWidth}px`).addClass(widthChanged ? 'green' : '');
-        $container.find('.bounds-info-after .bounds-info-height').text(`${sizes.image1[0].naturalHeight}px`).addClass(heightChanged ? 'green' : '');
-      }
-      if (sizes.image2.length !== 0) {
-        $container.find('.bounds-info-before .bounds-info-width').text(`${sizes.image2[0].naturalWidth}px`).addClass(widthChanged ? 'red' : '');
-        $container.find('.bounds-info-before .bounds-info-height').text(`${sizes.image2[0].naturalHeight}px`).addClass(heightChanged ? 'red' : '');
+      const widthChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalWidth !== sizes.$image2[0].naturalWidth;
+      const heightChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalHeight !== sizes.$image2[0].naturalHeight;
+      if (sizes.$image1?.length) {
+        const boundsInfoAfterWidth = container.querySelector('.bounds-info-after .bounds-info-width');
+        boundsInfoAfterWidth.textContent = `${sizes.$image1[0].naturalWidth}px`;
+        if (widthChanged) boundsInfoAfterWidth.classList.add('green');
+
+        const boundsInfoAfterHeight = container.querySelector('.bounds-info-after .bounds-info-height');
+        boundsInfoAfterHeight.textContent = `${sizes.$image1[0].naturalHeight}px`;
+        if (heightChanged) boundsInfoAfterHeight.classList.add('green');
       }
 
-      sizes.image1.css({
-        width: sizes.size1.width * factor,
-        height: sizes.size1.height * factor
-      });
-      sizes.image1.parent().css({
-        margin: `10px auto`,
-        width: sizes.size1.width * factor + 2,
-        height: sizes.size1.height * factor + 2
-      });
-      sizes.image2.css({
-        width: sizes.size2.width * factor,
-        height: sizes.size2.height * factor
-      });
-      sizes.image2.parent().css({
-        margin: `10px auto`,
-        width: sizes.size2.width * factor + 2,
-        height: sizes.size2.height * factor + 2
-      });
+      if (sizes.$image2?.length) {
+        const boundsInfoBeforeWidth = container.querySelector('.bounds-info-before .bounds-info-width');
+        boundsInfoBeforeWidth.textContent = `${sizes.$image2[0].naturalWidth}px`;
+        if (widthChanged) boundsInfoBeforeWidth.classList.add('red');
+
+        const boundsInfoBeforeHeight = container.querySelector('.bounds-info-before .bounds-info-height');
+        boundsInfoBeforeHeight.textContent = `${sizes.$image2[0].naturalHeight}px`;
+        if (heightChanged) boundsInfoBeforeHeight.classList.add('red');
+      }
+
+      const image1 = sizes.$image1[0];
+      if (image1) {
+        const container = image1.parentNode;
+        image1.style.width = `${sizes.size1.width * factor}px`;
+        image1.style.height = `${sizes.size1.height * factor}px`;
+        container.style.margin = '10px auto';
+        container.style.width = `${sizes.size1.width * factor + 2}px`;
+        container.style.height = `${sizes.size1.height * factor + 2}px`;
+      }
+
+      const image2 = sizes.$image2[0];
+      if (image2) {
+        const container = image2.parentNode;
+        image2.style.width = `${sizes.size2.width * factor}px`;
+        image2.style.height = `${sizes.size2.height * factor}px`;
+        container.style.margin = '10px auto';
+        container.style.width = `${sizes.size2.width * factor + 2}px`;
+        container.style.height = `${sizes.size2.height * factor + 2}px`;
+      }
     }
 
     function initSwipe(sizes) {
@@ -159,36 +173,39 @@ export function initImageDiff() {
         factor = (diffContainerWidth - 12) / sizes.max.width;
       }
 
-      sizes.image1.css({
-        width: sizes.size1.width * factor,
-        height: sizes.size1.height * factor
-      });
-      sizes.image1.parent().css({
-        margin: `0px ${sizes.ratio[0] * factor}px`,
-        width: sizes.size1.width * factor + 2,
-        height: sizes.size1.height * factor + 2
-      });
-      sizes.image1.parent().parent().css({
-        padding: `${sizes.ratio[1] * factor}px 0 0 0`,
-        width: sizes.max.width * factor + 2
-      });
-      sizes.image2.css({
-        width: sizes.size2.width * factor,
-        height: sizes.size2.height * factor
-      });
-      sizes.image2.parent().css({
-        margin: `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`,
-        width: sizes.size2.width * factor + 2,
-        height: sizes.size2.height * factor + 2
-      });
-      sizes.image2.parent().parent().css({
-        width: sizes.max.width * factor + 2,
-        height: sizes.max.height * factor + 2
-      });
-      $container.find('.diff-swipe').css({
-        width: sizes.max.width * factor + 2,
-        height: sizes.max.height * factor + 30 /* extra height for inner "position: absolute" elements */,
-      });
+      const image1 = sizes.$image1[0];
+      if (image1) {
+        const container = image1.parentNode;
+        const swipeFrame = container.parentNode;
+        image1.style.width = `${sizes.size1.width * factor}px`;
+        image1.style.height = `${sizes.size1.height * factor}px`;
+        container.style.margin = `0px ${sizes.ratio[0] * factor}px`;
+        container.style.width = `${sizes.size1.width * factor + 2}px`;
+        container.style.height = `${sizes.size1.height * factor + 2}px`;
+        swipeFrame.style.padding = `${sizes.ratio[1] * factor}px 0 0 0`;
+        swipeFrame.style.width = `${sizes.max.width * factor + 2}px`;
+      }
+
+      const image2 = sizes.$image2[0];
+      if (image2) {
+        const container = image2.parentNode;
+        const swipeFrame = container.parentNode;
+        image2.style.width = `${sizes.size2.width * factor}px`;
+        image2.style.height = `${sizes.size2.height * factor}px`;
+        container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`;
+        container.style.width = `${sizes.size2.width * factor + 2}px`;
+        container.style.height = `${sizes.size2.height * factor + 2}px`;
+        swipeFrame.style.width = `${sizes.max.width * factor + 2}px`;
+        swipeFrame.style.height = `${sizes.max.height * factor + 2}px`;
+      }
+
+      // extra height for inner "position: absolute" elements
+      const swipe = $container.find('.diff-swipe')[0];
+      if (swipe) {
+        swipe.style.width = `${sizes.max.width * factor + 2}px`;
+        swipe.style.height = `${sizes.max.height * factor + 30}px`;
+      }
+
       $container.find('.swipe-bar').on('mousedown', function(e) {
         e.preventDefault();
 
@@ -200,13 +217,9 @@ export function initImageDiff() {
           e2.preventDefault();
 
           const value = Math.max(0, Math.min(e2.clientX - $swipeFrame.offset().left, width));
+          $swipeBar[0].style.left = `${value}px`;
+          $container.find('.swipe-container')[0].style.width = `${$swipeFrame.width() - value}px`;
 
-          $swipeBar.css({
-            left: value
-          });
-          $container.find('.swipe-container').css({
-            width: $swipeFrame.width() - value
-          });
           $(document).on('mouseup.diff-swipe', () => {
             $(document).off('.diff-swipe');
           });
@@ -220,38 +233,39 @@ export function initImageDiff() {
         factor = (diffContainerWidth - 12) / sizes.max.width;
       }
 
-      sizes.image1.css({
-        width: sizes.size1.width * factor,
-        height: sizes.size1.height * factor
-      });
-      sizes.image2.css({
-        width: sizes.size2.width * factor,
-        height: sizes.size2.height * factor
-      });
-      sizes.image1.parent().css({
-        margin: `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`,
-        width: sizes.size1.width * factor + 2,
-        height: sizes.size1.height * factor + 2
-      });
-      sizes.image2.parent().css({
-        margin: `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`,
-        width: sizes.size2.width * factor + 2,
-        height: sizes.size2.height * factor + 2
-      });
+      const image1 = sizes.$image1[0];
+      if (image1) {
+        const container = image1.parentNode;
+        image1.style.width = `${sizes.size1.width * factor}px`;
+        image1.style.height = `${sizes.size1.height * factor}px`;
+        container.style.margin = `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`;
+        container.style.width = `${sizes.size1.width * factor + 2}px`;
+        container.style.height = `${sizes.size1.height * factor + 2}px`;
+      }
 
-      // some inner elements are `position: absolute`, so the container's height must be large enough
-      // the "css(width, height)" is somewhat hacky and not easy to understand, it could be improved in the future
-      sizes.image2.parent().parent().css({
-        width: sizes.max.width * factor + 2,
-        height: sizes.max.height * factor + 2,
-      });
+      const image2 = sizes.$image2[0];
+      if (image2) {
+        const container = image2.parentNode;
+        const overlayFrame = container.parentNode;
+        image2.style.width = `${sizes.size2.width * factor}px`;
+        image2.style.height = `${sizes.size2.height * factor}px`;
+        container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`;
+        container.style.width = `${sizes.size2.width * factor + 2}px`;
+        container.style.height = `${sizes.size2.height * factor + 2}px`;
 
-      const $range = $container.find("input[type='range']");
-      const onInput = () => sizes.image1.parent().css({
-        opacity: $range.val() / 100
-      });
-      $range.on('input', onInput);
-      onInput();
+        // some inner elements are `position: absolute`, so the container's height must be large enough
+        overlayFrame.style.width = `${sizes.max.width * factor + 2}px`;
+        overlayFrame.style.height = `${sizes.max.height * factor + 2}px`;
+      }
+
+      const rangeInput = $container[0].querySelector('input[type="range"]');
+      function updateOpacity() {
+        if (sizes?.$image1?.[0]) {
+          sizes.$image1[0].parentNode.style.opacity = `${rangeInput.value / 100}`;
+        }
+      }
+      rangeInput?.addEventListener('input', updateOpacity);
+      updateOpacity();
     }
   });
 }
diff --git a/web_src/js/features/install.js b/web_src/js/features/install.js
index 9fda7f7d27..54ba3778f8 100644
--- a/web_src/js/features/install.js
+++ b/web_src/js/features/install.js
@@ -1,19 +1,17 @@
-import $ from 'jquery';
 import {hideElem, showElem} from '../utils/dom.js';
 import {GET} from '../modules/fetch.js';
 
 export function initInstall() {
-  const $page = $('.page-content.install');
-  if ($page.length === 0) {
+  const page = document.querySelector('.page-content.install');
+  if (!page) {
     return;
   }
-  if ($page.is('.post-install')) {
+  if (page.classList.contains('post-install')) {
     initPostInstall();
   } else {
     initPreInstall();
   }
 }
-
 function initPreInstall() {
   const defaultDbUser = 'gitea';
   const defaultDbName = 'gitea';
@@ -21,86 +19,85 @@ function initPreInstall() {
   const defaultDbHosts = {
     mysql: '127.0.0.1:3306',
     postgres: '127.0.0.1:5432',
-    mssql: '127.0.0.1:1433'
+    mssql: '127.0.0.1:1433',
   };
 
-  const $dbHost = $('#db_host');
-  const $dbUser = $('#db_user');
-  const $dbName = $('#db_name');
+  const dbHost = document.getElementById('db_host');
+  const dbUser = document.getElementById('db_user');
+  const dbName = document.getElementById('db_name');
 
   // Database type change detection.
-  $('#db_type').on('change', function () {
-    const dbType = $(this).val();
-    hideElem($('div[data-db-setting-for]'));
-    showElem($(`div[data-db-setting-for=${dbType}]`));
+  document.getElementById('db_type').addEventListener('change', function () {
+    const dbType = this.value;
+    hideElem('div[data-db-setting-for]');
+    showElem(`div[data-db-setting-for=${dbType}]`);
 
     if (dbType !== 'sqlite3') {
       // for most remote database servers
-      showElem($(`div[data-db-setting-for=common-host]`));
-      const lastDbHost = $dbHost.val();
+      showElem('div[data-db-setting-for=common-host]');
+      const lastDbHost = dbHost.value;
       const isDbHostDefault = !lastDbHost || Object.values(defaultDbHosts).includes(lastDbHost);
       if (isDbHostDefault) {
-        $dbHost.val(defaultDbHosts[dbType] ?? '');
+        dbHost.value = defaultDbHosts[dbType] ?? '';
       }
-      if (!$dbUser.val() && !$dbName.val()) {
-        $dbUser.val(defaultDbUser);
-        $dbName.val(defaultDbName);
+      if (!dbUser.value && !dbName.value) {
+        dbUser.value = defaultDbUser;
+        dbName.value = defaultDbName;
       }
     } // else: for SQLite3, the default path is always prepared by backend code (setting)
-  }).trigger('change');
+  });
+  document.getElementById('db_type').dispatchEvent(new Event('change'));
 
-  const $appUrl = $('#app_url');
-  const configAppUrl = $appUrl.val();
-  if (configAppUrl.includes('://localhost')) {
-    $appUrl.val(window.location.href);
+  const appUrl = document.getElementById('app_url');
+  if (appUrl.value.includes('://localhost')) {
+    appUrl.value = window.location.href;
   }
 
-  const $domain = $('#domain');
-  const configDomain = $domain.val().trim();
-  if (configDomain === 'localhost') {
-    $domain.val(window.location.hostname);
+  const domain = document.getElementById('domain');
+  if (domain.value.trim() === 'localhost') {
+    domain.value = window.location.hostname;
   }
 
   // TODO: better handling of exclusive relations.
-  $('#offline-mode input').on('change', function () {
-    if ($(this).is(':checked')) {
-      $('#disable-gravatar').checkbox('check');
-      $('#federated-avatar-lookup').checkbox('uncheck');
+  document.querySelector('#offline-mode input').addEventListener('change', function () {
+    if (this.checked) {
+      document.querySelector('#disable-gravatar input').checked = true;
+      document.querySelector('#federated-avatar-lookup input').checked = false;
     }
   });
-  $('#disable-gravatar input').on('change', function () {
-    if ($(this).is(':checked')) {
-      $('#federated-avatar-lookup').checkbox('uncheck');
+  document.querySelector('#disable-gravatar input').addEventListener('change', function () {
+    if (this.checked) {
+      document.querySelector('#federated-avatar-lookup input').checked = false;
     } else {
-      $('#offline-mode').checkbox('uncheck');
+      document.querySelector('#offline-mode input').checked = false;
     }
   });
-  $('#federated-avatar-lookup input').on('change', function () {
-    if ($(this).is(':checked')) {
-      $('#disable-gravatar').checkbox('uncheck');
-      $('#offline-mode').checkbox('uncheck');
+  document.querySelector('#federated-avatar-lookup input').addEventListener('change', function () {
+    if (this.checked) {
+      document.querySelector('#disable-gravatar input').checked = false;
+      document.querySelector('#offline-mode input').checked = false;
     }
   });
-  $('#enable-openid-signin input').on('change', function () {
-    if ($(this).is(':checked')) {
-      if (!$('#disable-registration input').is(':checked')) {
-        $('#enable-openid-signup').checkbox('check');
+  document.querySelector('#enable-openid-signin input').addEventListener('change', function () {
+    if (this.checked) {
+      if (!document.querySelector('#disable-registration input').checked) {
+        document.querySelector('#enable-openid-signup input').checked = true;
       }
     } else {
-      $('#enable-openid-signup').checkbox('uncheck');
+      document.querySelector('#enable-openid-signup input').checked = false;
     }
   });
-  $('#disable-registration input').on('change', function () {
-    if ($(this).is(':checked')) {
-      $('#enable-captcha').checkbox('uncheck');
-      $('#enable-openid-signup').checkbox('uncheck');
+  document.querySelector('#disable-registration input').addEventListener('change', function () {
+    if (this.checked) {
+      document.querySelector('#enable-captcha input').checked = false;
+      document.querySelector('#enable-openid-signup input').checked = false;
     } else {
-      $('#enable-openid-signup').checkbox('check');
+      document.querySelector('#enable-openid-signup input').checked = true;
     }
   });
-  $('#enable-captcha input').on('change', function () {
-    if ($(this).is(':checked')) {
-      $('#disable-registration').checkbox('uncheck');
+  document.querySelector('#enable-captcha input').addEventListener('change', function () {
+    if (this.checked) {
+      document.querySelector('#disable-registration input').checked = false;
     }
   });
 }
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index 4dcf02d2dc..2de640e674 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -1,6 +1,8 @@
 import $ from 'jquery';
+import {GET} from '../modules/fetch.js';
+import {toggleElem} from '../utils/dom.js';
 
-const {appSubUrl, csrfToken, notificationSettings, assetVersionEncoded} = window.config;
+const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config;
 let notificationSequenceNumber = 0;
 
 export function initNotificationsTable() {
@@ -27,25 +29,6 @@ export function initNotificationsTable() {
       e.target.closest('.notifications-item').setAttribute('data-remove', 'true');
     });
   }
-
-  $('#notification_table .button').on('click', function () {
-    (async () => {
-      const data = await updateNotification(
-        $(this).data('url'),
-        $(this).data('status'),
-        $(this).data('page'),
-        $(this).data('q'),
-        $(this).data('notification-id'),
-      );
-
-      if ($(data).data('sequence-number') === notificationSequenceNumber) {
-        $('#notification_div').replaceWith(data);
-        initNotificationsTable();
-      }
-      await updateNotificationCount();
-    })();
-    return false;
-  });
 }
 
 async function receiveUpdateCount(event) {
@@ -53,7 +36,7 @@ async function receiveUpdateCount(event) {
     const data = JSON.parse(event.data);
 
     for (const count of document.querySelectorAll('.notification_count')) {
-      count.classList.toggle('gt-hidden', data.Count === 0);
+      count.classList.toggle('tw-hidden', data.Count === 0);
       count.textContent = `${data.Count}`;
     }
     await updateNotificationTable();
@@ -63,9 +46,9 @@ async function receiveUpdateCount(event) {
 }
 
 export function initNotificationCount() {
-  const notificationCount = $('.notification_count');
+  const $notificationCount = $('.notification_count');
 
-  if (!notificationCount.length) {
+  if (!$notificationCount.length) {
     return;
   }
 
@@ -73,7 +56,7 @@ export function initNotificationCount() {
   const startPeriodicPoller = (timeout, lastCount) => {
     if (timeout <= 0 || !Number.isFinite(timeout)) return;
     usingPeriodicPoller = true;
-    lastCount = lastCount ?? notificationCount.text();
+    lastCount = lastCount ?? $notificationCount.text();
     setTimeout(async () => {
       await updateNotificationCountWithCallback(startPeriodicPoller, timeout, lastCount);
     }, timeout);
@@ -112,7 +95,7 @@ export function initNotificationCount() {
           type: 'close',
         });
         worker.port.close();
-        window.location.href = appSubUrl;
+        window.location.href = `${appSubUrl}/`;
       } else if (event.data.type === 'close') {
         worker.port.postMessage({
           type: 'close',
@@ -161,60 +144,49 @@ async function updateNotificationCountWithCallback(callback, timeout, lastCount)
 }
 
 async function updateNotificationTable() {
-  const notificationDiv = $('#notification_div');
-  if (notificationDiv.length > 0) {
-    const data = await $.ajax({
-      type: 'GET',
-      url: `${appSubUrl}/notifications${window.location.search}`,
-      data: {
-        'div-only': true,
-        'sequence-number': ++notificationSequenceNumber,
+  const notificationDiv = document.getElementById('notification_div');
+  if (notificationDiv) {
+    try {
+      const params = new URLSearchParams(window.location.search);
+      params.set('div-only', true);
+      params.set('sequence-number', ++notificationSequenceNumber);
+      const url = `${appSubUrl}/notifications?${params.toString()}`;
+      const response = await GET(url);
+
+      if (!response.ok) {
+        throw new Error('Failed to fetch notification table');
       }
-    });
-    if ($(data).data('sequence-number') === notificationSequenceNumber) {
-      notificationDiv.replaceWith(data);
-      initNotificationsTable();
+
+      const data = await response.text();
+      if ($(data).data('sequence-number') === notificationSequenceNumber) {
+        notificationDiv.outerHTML = data;
+        initNotificationsTable();
+      }
+    } catch (error) {
+      console.error(error);
     }
   }
 }
 
 async function updateNotificationCount() {
-  const data = await $.ajax({
-    type: 'GET',
-    url: `${appSubUrl}/notifications/new`,
-    headers: {
-      'X-Csrf-Token': csrfToken,
-    },
-  });
+  try {
+    const response = await GET(`${appSubUrl}/notifications/new`);
 
-  const notificationCount = $('.notification_count');
-  if (data.new === 0) {
-    notificationCount.addClass('gt-hidden');
-  } else {
-    notificationCount.removeClass('gt-hidden');
+    if (!response.ok) {
+      throw new Error('Failed to fetch notification count');
+    }
+
+    const data = await response.json();
+
+    toggleElem('.notification_count', data.new !== 0);
+
+    for (const el of document.getElementsByClassName('notification_count')) {
+      el.textContent = `${data.new}`;
+    }
+
+    return `${data.new}`;
+  } catch (error) {
+    console.error(error);
+    return '0';
   }
-
-  notificationCount.text(`${data.new}`);
-
-  return `${data.new}`;
-}
-
-async function updateNotification(url, status, page, q, notificationID) {
-  if (status !== 'pinned') {
-    $(`#notification_${notificationID}`).remove();
-  }
-
-  return $.ajax({
-    type: 'POST',
-    url,
-    data: {
-      _csrf: csrfToken,
-      notification_id: notificationID,
-      status,
-      page,
-      q,
-      noredirect: true,
-      'sequence-number': ++notificationSequenceNumber,
-    },
-  });
 }
diff --git a/web_src/js/features/org-team.js b/web_src/js/features/org-team.js
index 6ae3a90f4d..c216fdf6a2 100644
--- a/web_src/js/features/org-team.js
+++ b/web_src/js/features/org-team.js
@@ -8,9 +8,9 @@ export function initOrgTeamSettings() {
   $('.organization.new.team input[name=permission]').on('change', () => {
     const val = $('input[name=permission]:checked', '.organization.new.team').val();
     if (val === 'admin') {
-      hideElem($('.organization.new.team .team-units'));
+      hideElem('.organization.new.team .team-units');
     } else {
-      showElem($('.organization.new.team .team-units'));
+      showElem('.organization.new.team .team-units');
     }
   });
 }
@@ -26,14 +26,14 @@ export function initOrgTeamSearchRepoBox() {
         $.each(response.data, (_i, item) => {
           items.push({
             title: item.repository.full_name.split('/')[1],
-            description: item.repository.full_name
+            description: item.repository.full_name,
           });
         });
 
         return {results: items};
-      }
+      },
     },
     searchFields: ['full_name'],
-    showNoResults: false
+    showNoResults: false,
   });
 }
diff --git a/web_src/js/features/pull-view-file.js b/web_src/js/features/pull-view-file.js
index 90881ee989..2472e5a0bd 100644
--- a/web_src/js/features/pull-view-file.js
+++ b/web_src/js/features/pull-view-file.js
@@ -43,9 +43,11 @@ export function initViewedCheckboxListenerFor() {
       // Mark the file as viewed visually - will especially change the background
       if (this.checked) {
         form.classList.add(viewedStyleClass);
+        checkbox.setAttribute('checked', '');
         prReview.numberOfViewedFiles++;
       } else {
         form.classList.remove(viewedStyleClass);
+        checkbox.removeAttribute('checked');
         prReview.numberOfViewedFiles--;
       }
 
diff --git a/web_src/js/features/recent-commits.js b/web_src/js/features/recent-commits.js
new file mode 100644
index 0000000000..030c251a05
--- /dev/null
+++ b/web_src/js/features/recent-commits.js
@@ -0,0 +1,21 @@
+import {createApp} from 'vue';
+
+export async function initRepoRecentCommits() {
+  const el = document.getElementById('repo-recent-commits-chart');
+  if (!el) return;
+
+  const {default: RepoRecentCommits} = await import(/* webpackChunkName: "recent-commits-graph" */'../components/RepoRecentCommits.vue');
+  try {
+    const View = createApp(RepoRecentCommits, {
+      locale: {
+        loadingTitle: el.getAttribute('data-locale-loading-title'),
+        loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
+        loadingInfo: el.getAttribute('data-locale-loading-info'),
+      },
+    });
+    View.mount(el);
+  } catch (err) {
+    console.error('RepoRecentCommits failed to load', err);
+    el.textContent = el.getAttribute('data-locale-component-failed-to-load');
+  }
+}
diff --git a/web_src/js/features/repo-branch.js b/web_src/js/features/repo-branch.js
index e6da9661b6..b9ffc6127f 100644
--- a/web_src/js/features/repo-branch.js
+++ b/web_src/js/features/repo-branch.js
@@ -8,35 +8,35 @@ export function initRepoBranchButton() {
 
 function initRepoCreateBranchButton() {
   // 2 pages share this code, one is the branch list page, the other is the commit view page: create branch/tag from current commit (dirty code)
-  $('.show-create-branch-modal').on('click', function () {
-    let modalFormName = $(this).attr('data-modal-form');
-    if (!modalFormName) {
-      modalFormName = '#create-branch-form';
-    }
-    $(modalFormName)[0].action = $(modalFormName).attr('data-base-action') + $(this).attr('data-branch-from-urlcomponent');
-    let fromSpanName = $(this).attr('data-modal-from-span');
-    if (!fromSpanName) {
-      fromSpanName = '#modal-create-branch-from-span';
-    }
+  for (const el of document.querySelectorAll('.show-create-branch-modal')) {
+    el.addEventListener('click', () => {
+      const modalFormName = el.getAttribute('data-modal-form') || '#create-branch-form';
+      const modalForm = document.querySelector(modalFormName);
+      if (!modalForm) return;
+      modalForm.action = `${modalForm.getAttribute('data-base-action')}${el.getAttribute('data-branch-from-urlcomponent')}`;
 
-    $(fromSpanName).text($(this).attr('data-branch-from'));
-    $($(this).attr('data-modal')).modal('show');
-  });
+      const fromSpanName = el.getAttribute('data-modal-from-span') || '#modal-create-branch-from-span';
+      document.querySelector(fromSpanName).textContent = el.getAttribute('data-branch-from');
+
+      $(el.getAttribute('data-modal')).modal('show');
+    });
+  }
 }
 
 function initRepoRenameBranchButton() {
-  $('.show-rename-branch-modal').on('click', function () {
-    const target = $(this).attr('data-modal');
-    const $modal = $(target);
+  for (const el of document.querySelectorAll('.show-rename-branch-modal')) {
+    el.addEventListener('click', () => {
+      const target = el.getAttribute('data-modal');
+      const modal = document.querySelector(target);
+      const oldBranchName = el.getAttribute('data-old-branch-name');
+      modal.querySelector('input[name=from]').value = oldBranchName;
 
-    const oldBranchName = $(this).attr('data-old-branch-name');
-    $modal.find('input[name=from]').val(oldBranchName);
+      // display the warning that the branch which is chosen is the default branch
+      const warn = modal.querySelector('.default-branch-warning');
+      toggleElem(warn, el.getAttribute('data-is-default-branch') === 'true');
 
-    // display the warning that the branch which is chosen is the default branch
-    const $warn = $modal.find('.default-branch-warning');
-    toggleElem($warn, $(this).attr('data-is-default-branch') === 'true');
-
-    const $text = $modal.find('[data-rename-branch-to]');
-    $text.text($text.attr('data-rename-branch-to').replace('%s', oldBranchName));
-  });
+      const text = modal.querySelector('[data-rename-branch-to]');
+      text.textContent = text.getAttribute('data-rename-branch-to').replace('%s', oldBranchName);
+    });
+  }
 }
diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js
index 306f38829f..63da5f2039 100644
--- a/web_src/js/features/repo-code.js
+++ b/web_src/js/features/repo-code.js
@@ -16,48 +16,52 @@ function changeHash(hash) {
   }
 }
 
-function selectRange($list, $select, $from) {
-  $list.removeClass('active');
+function isBlame() {
+  return Boolean(document.querySelector('div.blame'));
+}
+
+function getLineEls() {
+  return document.querySelectorAll(`.code-view td.lines-code${isBlame() ? '.blame-code' : ''}`);
+}
+
+function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
+  for (const el of $linesEls) {
+    el.closest('tr').classList.remove('active');
+  }
 
   // add hashchange to permalink
-  const $refInNewIssue = $('a.ref-in-new-issue');
-  const $copyPermalink = $('a.copy-line-permalink');
-  const $viewGitBlame = $('a.view_git_blame');
+  const refInNewIssue = document.querySelector('a.ref-in-new-issue');
+  const copyPermalink = document.querySelector('a.copy-line-permalink');
+  const viewGitBlame = document.querySelector('a.view_git_blame');
 
   const updateIssueHref = function (anchor) {
-    if ($refInNewIssue.length === 0) {
-      return;
-    }
-    const urlIssueNew = $refInNewIssue.attr('data-url-issue-new');
-    const urlParamBodyLink = $refInNewIssue.attr('data-url-param-body-link');
+    if (!refInNewIssue) return;
+    const urlIssueNew = refInNewIssue.getAttribute('data-url-issue-new');
+    const urlParamBodyLink = refInNewIssue.getAttribute('data-url-param-body-link');
     const issueContent = `${toAbsoluteUrl(urlParamBodyLink)}#${anchor}`; // the default content for issue body
-    $refInNewIssue.attr('href', `${urlIssueNew}?body=${encodeURIComponent(issueContent)}`);
+    refInNewIssue.setAttribute('href', `${urlIssueNew}?body=${encodeURIComponent(issueContent)}`);
   };
 
   const updateViewGitBlameFragment = function (anchor) {
-    if ($viewGitBlame.length === 0) {
-      return;
-    }
-    let href = $viewGitBlame.attr('href');
+    if (!viewGitBlame) return;
+    let href = viewGitBlame.getAttribute('href');
     href = `${href.replace(/#L\d+$|#L\d+-L\d+$/, '')}`;
     if (anchor.length !== 0) {
       href = `${href}#${anchor}`;
     }
-    $viewGitBlame.attr('href', href);
+    viewGitBlame.setAttribute('href', href);
   };
 
-  const updateCopyPermalinkUrl = function(anchor) {
-    if ($copyPermalink.length === 0) {
-      return;
-    }
-    let link = $copyPermalink.attr('data-url');
+  const updateCopyPermalinkUrl = function (anchor) {
+    if (!copyPermalink) return;
+    let link = copyPermalink.getAttribute('data-url');
     link = `${link.replace(/#L\d+$|#L\d+-L\d+$/, '')}#${anchor}`;
-    $copyPermalink.attr('data-url', link);
+    copyPermalink.setAttribute('data-url', link);
   };
 
-  if ($from) {
-    let a = parseInt($select.attr('rel').slice(1));
-    let b = parseInt($from.attr('rel').slice(1));
+  if ($selectionStartEls) {
+    let a = parseInt($selectionEndEl[0].getAttribute('rel').slice(1));
+    let b = parseInt($selectionStartEls[0].getAttribute('rel').slice(1));
     let c;
     if (a !== b) {
       if (a > b) {
@@ -69,7 +73,9 @@ function selectRange($list, $select, $from) {
       for (let i = a; i <= b; i++) {
         classes.push(`[rel=L${i}]`);
       }
-      $list.filter(classes.join(',')).addClass('active');
+      $linesEls.filter(classes.join(',')).each(function () {
+        this.closest('tr').classList.add('active');
+      });
       changeHash(`#L${a}-L${b}`);
 
       updateIssueHref(`L${a}-L${b}`);
@@ -78,12 +84,12 @@ function selectRange($list, $select, $from) {
       return;
     }
   }
-  $select.addClass('active');
-  changeHash(`#${$select.attr('rel')}`);
+  $selectionEndEl[0].closest('tr').classList.add('active');
+  changeHash(`#${$selectionEndEl[0].getAttribute('rel')}`);
 
-  updateIssueHref($select.attr('rel'));
-  updateViewGitBlameFragment($select.attr('rel'));
-  updateCopyPermalinkUrl($select.attr('rel'));
+  updateIssueHref($selectionEndEl[0].getAttribute('rel'));
+  updateViewGitBlameFragment($selectionEndEl[0].getAttribute('rel'));
+  updateCopyPermalinkUrl($selectionEndEl[0].getAttribute('rel'));
 }
 
 function showLineButton() {
@@ -96,10 +102,10 @@ function showLineButton() {
   }
 
   // find active row and add button
-  const tr = document.querySelector('.code-view td.lines-code.active').closest('tr');
-  const td = tr.querySelector('td');
+  const tr = document.querySelector('.code-view tr.active');
+  const td = tr.querySelector('td.lines-num');
   const btn = document.createElement('button');
-  btn.classList.add('code-line-button');
+  btn.classList.add('code-line-button', 'ui', 'basic', 'button');
   btn.innerHTML = svg('octicon-kebab-horizontal');
   td.prepend(btn);
 
@@ -116,21 +122,25 @@ function showLineButton() {
       tippy.popper.addEventListener('click', () => {
         tippy.hide();
       }, {once: true});
-    }
+    },
   });
 }
 
 export function initRepoCodeView() {
   if ($('.code-view .lines-num').length > 0) {
     $(document).on('click', '.lines-num span', function (e) {
-      const $select = $(this);
-      let $list;
-      if ($('div.blame').length) {
-        $list = $('.code-view td.lines-code.blame-code');
-      } else {
-        $list = $('.code-view td.lines-code');
+      const linesEls = getLineEls();
+      const selectedEls = Array.from(linesEls).filter((el) => {
+        return el.matches(`[rel=${this.getAttribute('id')}]`);
+      });
+
+      let from;
+      if (e.shiftKey) {
+        from = Array.from(linesEls).filter((el) => {
+          return el.closest('tr').classList.contains('active');
+        });
       }
-      selectRange($list, $list.filter(`[rel=${$select.attr('id')}]`), (e.shiftKey ? $list.filter('.active').eq(0) : null));
+      selectRange($(linesEls), $(selectedEls), from ? $(from) : null);
 
       if (window.getSelection) {
         window.getSelection().removeAllRanges();
@@ -138,28 +148,20 @@ export function initRepoCodeView() {
         document.selection.empty();
       }
 
-      // show code view menu marker (don't show in blame page)
-      if ($('div.blame').length === 0) {
-        showLineButton();
-      }
+      showLineButton();
     });
 
     $(window).on('hashchange', () => {
       let m = window.location.hash.match(rangeAnchorRegex);
-      let $list;
-      if ($('div.blame').length) {
-        $list = $('.code-view td.lines-code.blame-code');
-      } else {
-        $list = $('.code-view td.lines-code');
-      }
+      const $linesEls = $(getLineEls());
       let $first;
       if (m) {
-        $first = $list.filter(`[rel=${m[1]}]`);
+        $first = $linesEls.filter(`[rel=${m[1]}]`);
         if ($first.length) {
-          selectRange($list, $first, $list.filter(`[rel=${m[2]}]`));
+          selectRange($linesEls, $first, $linesEls.filter(`[rel=${m[2]}]`));
 
           // show code view menu marker (don't show in blame page)
-          if ($('div.blame').length === 0) {
+          if (!isBlame()) {
             showLineButton();
           }
 
@@ -169,12 +171,12 @@ export function initRepoCodeView() {
       }
       m = window.location.hash.match(singleAnchorRegex);
       if (m) {
-        $first = $list.filter(`[rel=L${m[2]}]`);
+        $first = $linesEls.filter(`[rel=L${m[2]}]`);
         if ($first.length) {
-          selectRange($list, $first);
+          selectRange($linesEls, $first);
 
           // show code view menu marker (don't show in blame page)
-          if ($('div.blame').length === 0) {
+          if (!isBlame()) {
             showLineButton();
           }
 
@@ -186,15 +188,7 @@ export function initRepoCodeView() {
   $(document).on('click', '.fold-file', ({currentTarget}) => {
     invertFileFolding(currentTarget.closest('.file-content'), currentTarget);
   });
-  $(document).on('click', '.code-expander-button', async ({currentTarget}) => {
-    const url = currentTarget.getAttribute('data-url');
-    const query = currentTarget.getAttribute('data-query');
-    const anchor = currentTarget.getAttribute('data-anchor');
-    if (!url) return;
-    const blob = await $.get(`${url}?${query}&anchor=${anchor}`);
-    currentTarget.closest('tr').outerHTML = blob;
-  });
-  $(document).on('click', '.copy-line-permalink', async (e) => {
-    await clippie(toAbsoluteUrl(e.currentTarget.getAttribute('data-url')));
+  $(document).on('click', '.copy-line-permalink', async ({currentTarget}) => {
+    await clippie(toAbsoluteUrl(currentTarget.getAttribute('data-url')));
   });
 }
diff --git a/web_src/js/features/repo-commit.js b/web_src/js/features/repo-commit.js
index 76b34d2077..f61ea08a42 100644
--- a/web_src/js/features/repo-commit.js
+++ b/web_src/js/features/repo-commit.js
@@ -1,72 +1,27 @@
-import $ from 'jquery';
 import {createTippy} from '../modules/tippy.js';
 import {toggleElem} from '../utils/dom.js';
 
-const {csrfToken} = window.config;
-
 export function initRepoEllipsisButton() {
-  $('.js-toggle-commit-body').on('click', function (e) {
-    e.preventDefault();
-    const expanded = $(this).attr('aria-expanded') === 'true';
-    toggleElem($(this).parent().find('.commit-body'));
-    $(this).attr('aria-expanded', String(!expanded));
-  });
-}
-
-export function initRepoCommitLastCommitLoader() {
-  const entryMap = {};
-
-  const entries = $('table#repo-files-table tr.notready')
-    .map((_, v) => {
-      entryMap[$(v).attr('data-entryname')] = $(v);
-      return $(v).attr('data-entryname');
-    })
-    .get();
-
-  if (entries.length === 0) {
-    return;
-  }
-
-  const lastCommitLoaderURL = $('table#repo-files-table').data('lastCommitLoaderUrl');
-
-  if (entries.length > 200) {
-    $.post(lastCommitLoaderURL, {
-      _csrf: csrfToken,
-    }, (data) => {
-      $('table#repo-files-table').replaceWith(data);
+  for (const button of document.querySelectorAll('.js-toggle-commit-body')) {
+    button.addEventListener('click', function (e) {
+      e.preventDefault();
+      const expanded = this.getAttribute('aria-expanded') === 'true';
+      toggleElem(this.parentElement.querySelector('.commit-body'));
+      this.setAttribute('aria-expanded', String(!expanded));
     });
-    return;
   }
-
-  $.post(lastCommitLoaderURL, {
-    _csrf: csrfToken,
-    'f': entries,
-  }, (data) => {
-    $(data).find('tr').each((_, row) => {
-      if (row.className === 'commit-list') {
-        $('table#repo-files-table .commit-list').replaceWith(row);
-        return;
-      }
-      // there are other <tr> rows in response (eg: <tr class="has-parent">)
-      // at the moment only the "data-entryname" rows should be processed
-      const entryName = $(row).attr('data-entryname');
-      if (entryName) {
-        entryMap[entryName].replaceWith(row);
-      }
-    });
-  });
 }
 
 export function initCommitStatuses() {
-  $('[data-tippy="commit-statuses"]').each(function () {
-    const top = $('.repository.file.list').length > 0 || $('.repository.diff').length > 0;
+  for (const element of document.querySelectorAll('[data-tippy="commit-statuses"]')) {
+    const top = document.querySelector('.repository.file.list') || document.querySelector('.repository.diff');
 
-    createTippy(this, {
-      content: this.nextElementSibling,
+    createTippy(element, {
+      content: element.nextElementSibling,
       placement: top ? 'top-start' : 'bottom-start',
       interactive: true,
       role: 'dialog',
       theme: 'box-with-header',
     });
-  });
+  }
 }
diff --git a/web_src/js/features/repo-common.js b/web_src/js/features/repo-common.js
index 3573e4d50b..b750addb07 100644
--- a/web_src/js/features/repo-common.js
+++ b/web_src/js/features/repo-common.js
@@ -1,44 +1,42 @@
 import $ from 'jquery';
 import {hideElem, showElem} from '../utils/dom.js';
+import {POST} from '../modules/fetch.js';
 
-const {csrfToken} = window.config;
+async function getArchive($target, url, first) {
+  const dropdownBtn = $target[0].closest('.ui.dropdown.button') ?? $target[0].closest('.ui.dropdown.btn');
 
-function getArchive($target, url, first) {
-  $.ajax({
-    url,
-    type: 'POST',
-    data: {
-      _csrf: csrfToken,
-    },
-    complete(xhr) {
-      if (xhr.status === 200) {
-        if (!xhr.responseJSON) {
-          // XXX Shouldn't happen?
-          $target.closest('.dropdown').children('i').removeClass('loading');
-          return;
-        }
-
-        if (!xhr.responseJSON.complete) {
-          $target.closest('.dropdown').children('i').addClass('loading');
-          // Wait for only three quarters of a second initially, in case it's
-          // quickly archived.
-          setTimeout(() => {
-            getArchive($target, url, false);
-          }, first ? 750 : 2000);
-        } else {
-          // We don't need to continue checking.
-          $target.closest('.dropdown').children('i').removeClass('loading');
-          window.location.href = url;
-        }
+  try {
+    dropdownBtn.classList.add('is-loading');
+    const response = await POST(url);
+    if (response.status === 200) {
+      const data = await response.json();
+      if (!data) {
+        // XXX Shouldn't happen?
+        dropdownBtn.classList.remove('is-loading');
+        return;
       }
-    },
-  });
+
+      if (!data.complete) {
+        // Wait for only three quarters of a second initially, in case it's
+        // quickly archived.
+        setTimeout(() => {
+          getArchive($target, url, false);
+        }, first ? 750 : 2000);
+      } else {
+        // We don't need to continue checking.
+        dropdownBtn.classList.remove('is-loading');
+        window.location.href = url;
+      }
+    }
+  } catch {
+    dropdownBtn.classList.remove('is-loading');
+  }
 }
 
 export function initRepoArchiveLinks() {
   $('.archive-link').on('click', function (event) {
     event.preventDefault();
-    const url = $(this).attr('href');
+    const url = this.getAttribute('href');
     if (!url) return;
     getArchive($(event.target), url, true);
   });
@@ -80,14 +78,16 @@ export function initRepoCommonBranchOrTagDropdown(selector) {
 
 export function initRepoCommonFilterSearchDropdown(selector) {
   const $dropdown = $(selector);
+  if (!$dropdown.length) return;
+
   $dropdown.dropdown({
     fullTextSearch: 'exact',
     selectOnKeydown: false,
     onChange(_text, _value, $choice) {
-      if ($choice.attr('data-url')) {
-        window.location.href = $choice.attr('data-url');
+      if ($choice[0].getAttribute('data-url')) {
+        window.location.href = $choice[0].getAttribute('data-url');
       }
     },
-    message: {noResults: $dropdown.attr('data-no-results')},
+    message: {noResults: $dropdown[0].getAttribute('data-no-results')},
   });
 }
diff --git a/web_src/js/features/repo-diff-commit.js b/web_src/js/features/repo-diff-commit.js
index 3d4f0f677a..aa7fc38360 100644
--- a/web_src/js/features/repo-diff-commit.js
+++ b/web_src/js/features/repo-diff-commit.js
@@ -35,11 +35,11 @@ function addBranches(area, branches, defaultBranch) {
 
 function addLink(parent, href, text, tooltip) {
   const link = document.createElement('a');
-  link.classList.add('muted', 'gt-px-2');
+  link.classList.add('muted', 'tw-px-1');
   link.href = href;
   link.textContent = text;
   if (tooltip) {
-    link.classList.add('gt-border-secondary', 'gt-rounded');
+    link.classList.add('tw-border', 'tw-border-secondary', 'tw-rounded');
     link.setAttribute('data-tooltip-content', tooltip);
   }
   parent.append(link);
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index eeb80e91b2..b2e8ec4866 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -7,38 +7,46 @@ import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js';
 import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js';
 import {initImageDiff} from './imagediff.js';
 import {showErrorToast} from '../modules/toast.js';
-import {submitEventSubmitter} from '../utils/dom.js';
+import {submitEventSubmitter, queryElemSiblings, hideElem, showElem} from '../utils/dom.js';
+import {POST, GET} from '../modules/fetch.js';
 
-const {csrfToken, pageData, i18n} = window.config;
+const {pageData, i18n} = window.config;
 
 function initRepoDiffReviewButton() {
-  const $reviewBox = $('#review-box');
-  const $counter = $reviewBox.find('.review-comments-counter');
+  const reviewBox = document.getElementById('review-box');
+  if (!reviewBox) return;
+
+  const counter = reviewBox.querySelector('.review-comments-counter');
+  if (!counter) return;
 
   $(document).on('click', 'button[name="pending_review"]', (e) => {
     const $form = $(e.target).closest('form');
     // Watch for the form's submit event.
     $form.on('submit', () => {
-      const num = parseInt($counter.attr('data-pending-comment-number')) + 1 || 1;
-      $counter.attr('data-pending-comment-number', num);
-      $counter.text(num);
-      // Force the browser to reflow the DOM. This is to ensure that the browser replay the animation
-      $reviewBox.removeClass('pulse');
-      $reviewBox.width();
-      $reviewBox.addClass('pulse');
+      const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1;
+      counter.setAttribute('data-pending-comment-number', num);
+      counter.textContent = num;
+
+      reviewBox.classList.remove('pulse');
+      requestAnimationFrame(() => {
+        reviewBox.classList.add('pulse');
+      });
     });
   });
 }
 
 function initRepoDiffFileViewToggle() {
   $('.file-view-toggle').on('click', function () {
-    const $this = $(this);
-    $this.parent().children().removeClass('active');
-    $this.addClass('active');
+    for (const el of queryElemSiblings(this)) {
+      el.classList.remove('active');
+    }
+    this.classList.add('active');
 
-    const $target = $($this.data('toggle-selector'));
-    $target.parent().children().addClass('gt-hidden');
-    $target.removeClass('gt-hidden');
+    const target = document.querySelector(this.getAttribute('data-toggle-selector'));
+    if (!target) return;
+
+    hideElem(queryElemSiblings(target));
+    showElem(target);
   });
 }
 
@@ -47,38 +55,44 @@ function initRepoDiffConversationForm() {
     e.preventDefault();
 
     const $form = $(e.target);
-    const $textArea = $form.find('textarea');
-    if (!validateTextareaNonEmpty($textArea)) {
+    const textArea = e.target.querySelector('textarea');
+    if (!validateTextareaNonEmpty(textArea)) {
       return;
     }
 
-    if ($form.hasClass('is-loading')) return;
+    if (e.target.classList.contains('is-loading')) return;
     try {
-      $form.addClass('is-loading');
+      e.target.classList.add('is-loading');
       const formData = new FormData($form[0]);
 
       // if the form is submitted by a button, append the button's name and value to the form data
-      const submitter = submitEventSubmitter(e.originalEvent);
+      const submitter = submitEventSubmitter(e);
       const isSubmittedByButton = (submitter?.nodeName === 'BUTTON') || (submitter?.nodeName === 'INPUT' && submitter.type === 'submit');
       if (isSubmittedByButton && submitter.name) {
         formData.append(submitter.name, submitter.value);
       }
-      const formDataString = String(new URLSearchParams(formData));
-      const $newConversationHolder = $(await $.post($form.attr('action'), formDataString));
+
+      const response = await POST(e.target.getAttribute('action'), {data: formData});
+      const $newConversationHolder = $(await response.text());
       const {path, side, idx} = $newConversationHolder.data();
 
       $form.closest('.conversation-holder').replaceWith($newConversationHolder);
+      let selector;
       if ($form.closest('tr').data('line-type') === 'same') {
-        $(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).addClass('gt-invisible');
+        selector = `[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`;
       } else {
-        $(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).addClass('gt-invisible');
+        selector = `[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`;
+      }
+      for (const el of document.querySelectorAll(selector)) {
+        el.classList.add('tw-invisible');
       }
       $newConversationHolder.find('.dropdown').dropdown();
       initCompReactionSelector($newConversationHolder);
-    } catch { // here the caught error might be a jQuery AJAX error (thrown by await $.post), which is not good to use for error message handling
+    } catch (error) {
+      console.error('Error:', error);
       showErrorToast(i18n.network_error);
     } finally {
-      $form.removeClass('is-loading');
+      e.target.classList.remove('is-loading');
     }
   });
 
@@ -89,15 +103,20 @@ function initRepoDiffConversationForm() {
     const action = $(this).data('action');
     const url = $(this).data('update-url');
 
-    const data = await $.post(url, {_csrf: csrfToken, origin, action, comment_id});
+    try {
+      const response = await POST(url, {data: new URLSearchParams({origin, action, comment_id})});
+      const data = await response.text();
 
-    if ($(this).closest('.conversation-holder').length) {
-      const conversation = $(data);
-      $(this).closest('.conversation-holder').replaceWith(conversation);
-      conversation.find('.dropdown').dropdown();
-      initCompReactionSelector(conversation);
-    } else {
-      window.location.reload();
+      if ($(this).closest('.conversation-holder').length) {
+        const $conversation = $(data);
+        $(this).closest('.conversation-holder').replaceWith($conversation);
+        $conversation.find('.dropdown').dropdown();
+        initCompReactionSelector($conversation);
+      } else {
+        window.location.reload();
+      }
+    } catch (error) {
+      console.error('Error:', error);
     }
   });
 }
@@ -106,20 +125,20 @@ export function initRepoDiffConversationNav() {
   // Previous/Next code review conversation
   $(document).on('click', '.previous-conversation', (e) => {
     const $conversation = $(e.currentTarget).closest('.comment-code-cloud');
-    const $conversations = $('.comment-code-cloud:not(.gt-hidden)');
+    const $conversations = $('.comment-code-cloud:not(.tw-hidden)');
     const index = $conversations.index($conversation);
     const previousIndex = index > 0 ? index - 1 : $conversations.length - 1;
     const $previousConversation = $conversations.eq(previousIndex);
-    const anchor = $previousConversation.find('.comment').first().attr('id');
+    const anchor = $previousConversation.find('.comment').first()[0].getAttribute('id');
     window.location.href = `#${anchor}`;
   });
   $(document).on('click', '.next-conversation', (e) => {
     const $conversation = $(e.currentTarget).closest('.comment-code-cloud');
-    const $conversations = $('.comment-code-cloud:not(.gt-hidden)');
+    const $conversations = $('.comment-code-cloud:not(.tw-hidden)');
     const index = $conversations.index($conversation);
     const nextIndex = index < $conversations.length - 1 ? index + 1 : 0;
     const $nextConversation = $conversations.eq(nextIndex);
-    const anchor = $nextConversation.find('.comment').first().attr('id');
+    const anchor = $nextConversation.find('.comment').first()[0].getAttribute('id');
     window.location.href = `#${anchor}`;
   });
 }
@@ -132,18 +151,18 @@ function onShowMoreFiles() {
   initImageDiff();
 }
 
-export function loadMoreFiles(url) {
-  const $target = $('a#diff-show-more-files');
-  if ($target.hasClass('disabled') || pageData.diffFileInfo.isLoadingNewData) {
+export async function loadMoreFiles(url) {
+  const target = document.querySelector('a#diff-show-more-files');
+  if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) {
     return;
   }
 
   pageData.diffFileInfo.isLoadingNewData = true;
-  $target.addClass('disabled');
-  $.ajax({
-    type: 'GET',
-    url,
-  }).done((resp) => {
+  target?.classList.add('disabled');
+
+  try {
+    const response = await GET(url);
+    const resp = await response.text();
     const $resp = $(resp);
     // the response is a full HTML page, we need to extract the relevant contents:
     // 1. append the newly loaded file list items to the existing list
@@ -152,52 +171,55 @@ export function loadMoreFiles(url) {
     $('body').append($resp.find('script#diff-data-script'));
 
     onShowMoreFiles();
-  }).always(() => {
-    $target.removeClass('disabled');
+  } catch (error) {
+    console.error('Error:', error);
+    showErrorToast('An error occurred while loading more files.');
+  } finally {
+    target?.classList.remove('disabled');
     pageData.diffFileInfo.isLoadingNewData = false;
-  });
+  }
 }
 
 function initRepoDiffShowMore() {
   $(document).on('click', 'a#diff-show-more-files', (e) => {
     e.preventDefault();
 
-    const $target = $(e.target);
-    const linkLoadMore = $target.attr('data-href');
+    const linkLoadMore = e.target.getAttribute('data-href');
     loadMoreFiles(linkLoadMore);
   });
 
-  $(document).on('click', 'a.diff-load-button', (e) => {
+  $(document).on('click', 'a.diff-load-button', async (e) => {
     e.preventDefault();
     const $target = $(e.target);
 
-    if ($target.hasClass('disabled')) {
+    if (e.target.classList.contains('disabled')) {
       return;
     }
 
-    $target.addClass('disabled');
+    e.target.classList.add('disabled');
 
     const url = $target.data('href');
-    $.ajax({
-      type: 'GET',
-      url,
-    }).done((resp) => {
+
+    try {
+      const response = await GET(url);
+      const resp = await response.text();
+
       if (!resp) {
-        $target.removeClass('disabled');
         return;
       }
       $target.parent().replaceWith($(resp).find('#diff-file-boxes .diff-file-body .file-body').children());
       onShowMoreFiles();
-    }).fail(() => {
-      $target.removeClass('disabled');
-    });
+    } catch (error) {
+      console.error('Error:', error);
+    } finally {
+      e.target.classList.remove('disabled');
+    }
   });
 }
 
 export function initRepoDiffView() {
   initRepoDiffConversationForm();
-  const diffFileList = $('#diff-file-list');
-  if (diffFileList.length === 0) return;
+  if (!$('#diff-file-list').length) return;
   initDiffFileTree();
   initDiffCommitSelect();
   initRepoDiffShowMore();
diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index f00f817223..01dc4b95aa 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -4,60 +4,44 @@ import {createCodeEditor} from './codeeditor.js';
 import {hideElem, showElem} from '../utils/dom.js';
 import {initMarkupContent} from '../markup/content.js';
 import {attachRefIssueContextPopup} from './contextpopup.js';
-
-const {csrfToken} = window.config;
+import {POST} from '../modules/fetch.js';
 
 function initEditPreviewTab($form) {
   const $tabMenu = $form.find('.tabular.menu');
   $tabMenu.find('.item').tab();
   const $previewTab = $tabMenu.find(`.item[data-tab="${$tabMenu.data('preview')}"]`);
   if ($previewTab.length) {
-    $previewTab.on('click', function () {
+    $previewTab.on('click', async function () {
       const $this = $(this);
       let context = `${$this.data('context')}/`;
       const mode = $this.data('markup-mode') || 'comment';
-      const treePathEl = $form.find('input#tree_path');
-      if (treePathEl.length > 0) {
-        context += treePathEl.val();
+      const $treePathEl = $form.find('input#tree_path');
+      if ($treePathEl.length > 0) {
+        context += $treePathEl.val();
       }
       context = context.substring(0, context.lastIndexOf('/'));
-      $.post($this.data('url'), {
-        _csrf: csrfToken,
-        mode,
-        context,
-        text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(),
-        file_path: treePathEl.val(),
-      }, (data) => {
+
+      const formData = new FormData();
+      formData.append('mode', mode);
+      formData.append('context', context);
+      formData.append('text', $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val());
+      formData.append('file_path', $treePathEl.val());
+      try {
+        const response = await POST($this.data('url'), {data: formData});
+        const data = await response.text();
         const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`);
         renderPreviewPanelContent($previewPanel, data);
-      });
+      } catch (error) {
+        console.error('Error:', error);
+      }
     });
   }
 }
 
-function initEditDiffTab($form) {
-  const $tabMenu = $form.find('.tabular.menu');
-  $tabMenu.find('.item').tab();
-  $tabMenu.find(`.item[data-tab="${$tabMenu.data('diff')}"]`).on('click', function () {
-    const $this = $(this);
-    $.post($this.data('url'), {
-      _csrf: csrfToken,
-      context: $this.data('context'),
-      content: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(),
-    }, (data) => {
-      const $diffPreviewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('diff')}"]`);
-      $diffPreviewPanel.html(data);
-    });
-  });
-}
-
 function initEditorForm() {
-  if ($('.repository .edit.form').length === 0) {
-    return;
-  }
-
-  initEditPreviewTab($('.repository .edit.form'));
-  initEditDiffTab($('.repository .edit.form'));
+  const $form = $('.repository .edit.form');
+  if (!$form) return;
+  initEditPreviewTab($form);
 }
 
 function getCursorPosition($e) {
@@ -80,23 +64,23 @@ export function initRepoEditor() {
 
   $('.js-quick-pull-choice-option').on('change', function () {
     if ($(this).val() === 'commit-to-new-branch') {
-      showElem($('.quick-pull-branch-name'));
-      $('.quick-pull-branch-name input').prop('required', true);
+      showElem('.quick-pull-branch-name');
+      document.querySelector('.quick-pull-branch-name input').required = true;
     } else {
-      hideElem($('.quick-pull-branch-name'));
-      $('.quick-pull-branch-name input').prop('required', false);
+      hideElem('.quick-pull-branch-name');
+      document.querySelector('.quick-pull-branch-name input').required = false;
     }
-    $('#commit-button').text($(this).attr('button_text'));
+    $('#commit-button').text(this.getAttribute('button_text'));
   });
 
   const joinTreePath = ($fileNameEl) => {
     const parts = [];
     $('.breadcrumb span.section').each(function () {
-      const element = $(this);
-      if (element.find('a').length) {
-        parts.push(element.find('a').text());
+      const $element = $(this);
+      if ($element.find('a').length) {
+        parts.push($element.find('a').text());
       } else {
-        parts.push(element.text());
+        parts.push($element.text());
       }
     });
     if ($fileNameEl.val()) parts.push($fileNameEl.val());
@@ -149,13 +133,13 @@ export function initRepoEditor() {
 
     // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
     // to enable or disable the commit button
-    const $commitButton = $('#commit-button');
+    const commitButton = document.getElementById('commit-button');
     const $editForm = $('.ui.edit.form');
     const dirtyFileClass = 'dirty-file';
 
     // Disabling the button at the start
     if ($('input[name="page_has_posted"]').val() !== 'true') {
-      $commitButton.prop('disabled', true);
+      commitButton.disabled = true;
     }
 
     // Registering a custom listener for the file path and the file content
@@ -163,9 +147,9 @@ export function initRepoEditor() {
       silent: true,
       dirtyClass: dirtyFileClass,
       fieldSelector: ':input:not(.commit-form-wrapper :input)',
-      change() {
-        const dirty = $(this).hasClass(dirtyFileClass);
-        $commitButton.prop('disabled', !dirty);
+      change($form) {
+        const dirty = $form[0]?.classList.contains(dirtyFileClass);
+        commitButton.disabled = !dirty;
       },
     });
 
@@ -177,15 +161,15 @@ export function initRepoEditor() {
       editor.setValue(value);
     }
 
-    $commitButton.on('click', (event) => {
+    commitButton?.addEventListener('click', (e) => {
       // A modal which asks if an empty file should be committed
-      if ($editArea.val().length === 0) {
+      if (!$editArea.val()) {
         $('#edit-empty-content-modal').modal({
           onApprove() {
             $('.edit.form').trigger('submit');
           },
         }).modal('show');
-        event.preventDefault();
+        e.preventDefault();
       }
     });
   })();
@@ -195,6 +179,6 @@ export function renderPreviewPanelContent($panelPreviewer, data) {
   $panelPreviewer.html(data);
   initMarkupContent();
 
-  const refIssues = $panelPreviewer.find('p .ref-issue');
-  attachRefIssueContextPopup(refIssues);
+  const $refIssues = $panelPreviewer.find('p .ref-issue');
+  attachRefIssueContextPopup($refIssues);
 }
diff --git a/web_src/js/features/repo-findfile.js b/web_src/js/features/repo-findfile.js
index 158732acc2..cff5068a1e 100644
--- a/web_src/js/features/repo-findfile.js
+++ b/web_src/js/features/repo-findfile.js
@@ -1,13 +1,11 @@
-import $ from 'jquery';
 import {svg} from '../svg.js';
 import {toggleElem} from '../utils/dom.js';
 import {pathEscapeSegments} from '../utils/url.js';
-
-const {csrf} = window.config;
+import {GET} from '../modules/fetch.js';
 
 const threshold = 50;
 let files = [];
-let $repoFindFileInput, $repoFindFileTableBody, $repoFindFileNoResult;
+let repoFindFileInput, repoFindFileTableBody, repoFindFileNoResult;
 
 // return the case-insensitive sub-match result as an array:  [unmatched, matched, unmatched, matched, ...]
 // res[even] is unmatched, res[odd] is matched, see unit tests for examples
@@ -74,46 +72,46 @@ export function filterRepoFilesWeighted(files, filter) {
 }
 
 function filterRepoFiles(filter) {
-  const treeLink = $repoFindFileInput.attr('data-url-tree-link');
-  $repoFindFileTableBody.empty();
+  const treeLink = repoFindFileInput.getAttribute('data-url-tree-link');
+  repoFindFileTableBody.innerHTML = '';
 
   const filterResult = filterRepoFilesWeighted(files, filter);
-  const tmplRow = `<tr><td><a></a></td></tr>`;
 
-  toggleElem($repoFindFileNoResult, filterResult.length === 0);
+  toggleElem(repoFindFileNoResult, !filterResult.length);
   for (const r of filterResult) {
-    const $row = $(tmplRow);
-    const $a = $row.find('a');
-    $a.attr('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`);
-    const $octiconFile = $(svg('octicon-file')).addClass('gt-mr-3');
-    $a.append($octiconFile);
-    // if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz']
-    // the matchResult[odd] is matched and highlighted to red.
-    for (let j = 0; j < r.matchResult.length; j++) {
-      if (!r.matchResult[j]) continue;
-      const $span = $('<span>').text(r.matchResult[j]);
-      if (j % 2 === 1) $span.addClass('ui text red');
-      $a.append($span);
+    const row = document.createElement('tr');
+    const cell = document.createElement('td');
+    const a = document.createElement('a');
+    a.setAttribute('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`);
+    a.innerHTML = svg('octicon-file', 16, 'tw-mr-2');
+    row.append(cell);
+    cell.append(a);
+    for (const [index, part] of r.matchResult.entries()) {
+      const span = document.createElement('span');
+      // safely escape by using textContent
+      span.textContent = part;
+      // if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz']
+      // the matchResult[odd] is matched and highlighted to red.
+      if (index % 2 === 1) span.classList.add('ui', 'text', 'red');
+      a.append(span);
     }
-    $repoFindFileTableBody.append($row);
+    repoFindFileTableBody.append(row);
   }
 }
 
 async function loadRepoFiles() {
-  files = await $.ajax({
-    url: $repoFindFileInput.attr('data-url-data-link'),
-    headers: {'X-Csrf-Token': csrf}
-  });
-  filterRepoFiles($repoFindFileInput.val());
+  const response = await GET(repoFindFileInput.getAttribute('data-url-data-link'));
+  files = await response.json();
+  filterRepoFiles(repoFindFileInput.value);
 }
 
 export function initFindFileInRepo() {
-  $repoFindFileInput = $('#repo-file-find-input');
-  if (!$repoFindFileInput.length) return;
+  repoFindFileInput = document.getElementById('repo-file-find-input');
+  if (!repoFindFileInput) return;
 
-  $repoFindFileTableBody = $('#repo-find-file-table tbody');
-  $repoFindFileNoResult = $('#repo-find-file-no-result');
-  $repoFindFileInput.on('input', () => filterRepoFiles($repoFindFileInput.val()));
+  repoFindFileTableBody = document.querySelector('#repo-find-file-table tbody');
+  repoFindFileNoResult = document.getElementById('repo-find-file-no-result');
+  repoFindFileInput.addEventListener('input', () => filterRepoFiles(repoFindFileInput.value));
 
   loadRepoFiles();
 }
diff --git a/web_src/js/features/repo-graph.js b/web_src/js/features/repo-graph.js
index e445ae1103..0086b92021 100644
--- a/web_src/js/features/repo-graph.js
+++ b/web_src/js/features/repo-graph.js
@@ -1,13 +1,16 @@
 import $ from 'jquery';
+import {hideElem, showElem} from '../utils/dom.js';
+import {GET} from '../modules/fetch.js';
 
 export function initRepoGraphGit() {
   const graphContainer = document.getElementById('git-graph-container');
   if (!graphContainer) return;
 
-  $('#flow-color-monochrome').on('click', () => {
-    $('#flow-color-monochrome').addClass('active');
-    $('#flow-color-colored').removeClass('active');
-    $('#git-graph-container').removeClass('colored').addClass('monochrome');
+  document.getElementById('flow-color-monochrome')?.addEventListener('click', () => {
+    document.getElementById('flow-color-monochrome').classList.add('active');
+    document.getElementById('flow-color-colored')?.classList.remove('active');
+    graphContainer.classList.remove('colored');
+    graphContainer.classList.add('monochrome');
     const params = new URLSearchParams(window.location.search);
     params.set('mode', 'monochrome');
     const queryString = params.toString();
@@ -16,29 +19,31 @@ export function initRepoGraphGit() {
     } else {
       window.history.replaceState({}, '', window.location.pathname);
     }
-    $('.pagination a').each((_, that) => {
-      const href = $(that).attr('href');
-      if (!href) return;
+    for (const link of document.querySelectorAll('.pagination a')) {
+      const href = link.getAttribute('href');
+      if (!href) continue;
       const url = new URL(href, window.location);
       const params = url.searchParams;
       params.set('mode', 'monochrome');
       url.search = `?${params.toString()}`;
-      $(that).attr('href', url.href);
-    });
+      link.setAttribute('href', url.href);
+    }
   });
-  $('#flow-color-colored').on('click', () => {
-    $('#flow-color-colored').addClass('active');
-    $('#flow-color-monochrome').removeClass('active');
-    $('#git-graph-container').addClass('colored').removeClass('monochrome');
-    $('.pagination a').each((_, that) => {
-      const href = $(that).attr('href');
-      if (!href) return;
+
+  document.getElementById('flow-color-colored')?.addEventListener('click', () => {
+    document.getElementById('flow-color-colored').classList.add('active');
+    document.getElementById('flow-color-monochrome')?.classList.remove('active');
+    graphContainer.classList.add('colored');
+    graphContainer.classList.remove('monochrome');
+    for (const link of document.querySelectorAll('.pagination a')) {
+      const href = link.getAttribute('href');
+      if (!href) continue;
       const url = new URL(href, window.location);
       const params = url.searchParams;
       params.delete('mode');
       url.search = `?${params.toString()}`;
-      $(that).attr('href', url.href);
-    });
+      link.setAttribute('href', url.href);
+    }
     const params = new URLSearchParams(window.location.search);
     params.delete('mode');
     const queryString = params.toString();
@@ -55,18 +60,21 @@ export function initRepoGraphGit() {
     const ajaxUrl = new URL(url);
     ajaxUrl.searchParams.set('div-only', 'true');
     window.history.replaceState({}, '', queryString ? `?${queryString}` : window.location.pathname);
-    $('#pagination').empty();
-    $('#rel-container').addClass('gt-hidden');
-    $('#rev-container').addClass('gt-hidden');
-    $('#loading-indicator').removeClass('gt-hidden');
+    document.getElementById('pagination').innerHTML = '';
+    hideElem('#rel-container');
+    hideElem('#rev-container');
+    showElem('#loading-indicator');
     (async () => {
-      const div = $(await $.ajax(String(ajaxUrl)));
-      $('#pagination').html(div.find('#pagination').html());
-      $('#rel-container').html(div.find('#rel-container').html());
-      $('#rev-container').html(div.find('#rev-container').html());
-      $('#loading-indicator').addClass('gt-hidden');
-      $('#rel-container').removeClass('gt-hidden');
-      $('#rev-container').removeClass('gt-hidden');
+      const response = await GET(String(ajaxUrl));
+      const html = await response.text();
+      const div = document.createElement('div');
+      div.innerHTML = html;
+      document.getElementById('pagination').innerHTML = div.getElementById('pagination').innerHTML;
+      document.getElementById('rel-container').innerHTML = div.getElementById('rel-container').innerHTML;
+      document.getElementById('rev-container').innerHTML = div.getElementById('rev-container').innerHTML;
+      hideElem('#loading-indicator');
+      showElem('#rel-container');
+      showElem('#rev-container');
     })();
   };
   const dropdownSelected = params.getAll('branch');
@@ -74,8 +82,9 @@ export function initRepoGraphGit() {
     dropdownSelected.splice(0, 0, '...flow-hide-pr-refs');
   }
 
-  $('#flow-select-refs-dropdown').dropdown('set selected', dropdownSelected);
-  $('#flow-select-refs-dropdown').dropdown({
+  const flowSelectRefsDropdown = document.getElementById('flow-select-refs-dropdown');
+  $(flowSelectRefsDropdown).dropdown('set selected', dropdownSelected);
+  $(flowSelectRefsDropdown).dropdown({
     clearable: true,
     fullTextSeach: 'exact',
     onRemove(toRemove) {
@@ -101,36 +110,46 @@ export function initRepoGraphGit() {
       updateGraph();
     },
   });
-  $('#git-graph-container').on('mouseenter', '#rev-list li', (e) => {
-    const flow = $(e.currentTarget).data('flow');
-    if (flow === 0) return;
-    $(`#flow-${flow}`).addClass('highlight');
-    $(e.currentTarget).addClass('hover');
-    $(`#rev-list li[data-flow='${flow}']`).addClass('highlight');
+
+  graphContainer.addEventListener('mouseenter', (e) => {
+    if (e.target.matches('#rev-list li')) {
+      const flow = e.target.getAttribute('data-flow');
+      if (flow === '0') return;
+      document.getElementById(`flow-${flow}`)?.classList.add('highlight');
+      e.target.classList.add('hover');
+      for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
+        item.classList.add('highlight');
+      }
+    } else if (e.target.matches('#rel-container .flow-group')) {
+      e.target.classList.add('highlight');
+      const flow = e.target.getAttribute('data-flow');
+      for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
+        item.classList.add('highlight');
+      }
+    } else if (e.target.matches('#rel-container .flow-commit')) {
+      const rev = e.target.getAttribute('data-rev');
+      document.querySelector(`#rev-list li#commit-${rev}`)?.classList.add('hover');
+    }
   });
-  $('#git-graph-container').on('mouseleave', '#rev-list li', (e) => {
-    const flow = $(e.currentTarget).data('flow');
-    if (flow === 0) return;
-    $(`#flow-${flow}`).removeClass('highlight');
-    $(e.currentTarget).removeClass('hover');
-    $(`#rev-list li[data-flow='${flow}']`).removeClass('highlight');
-  });
-  $('#git-graph-container').on('mouseenter', '#rel-container .flow-group', (e) => {
-    $(e.currentTarget).addClass('highlight');
-    const flow = $(e.currentTarget).data('flow');
-    $(`#rev-list li[data-flow='${flow}']`).addClass('highlight');
-  });
-  $('#git-graph-container').on('mouseleave', '#rel-container .flow-group', (e) => {
-    $(e.currentTarget).removeClass('highlight');
-    const flow = $(e.currentTarget).data('flow');
-    $(`#rev-list li[data-flow='${flow}']`).removeClass('highlight');
-  });
-  $('#git-graph-container').on('mouseenter', '#rel-container .flow-commit', (e) => {
-    const rev = $(e.currentTarget).data('rev');
-    $(`#rev-list li#commit-${rev}`).addClass('hover');
-  });
-  $('#git-graph-container').on('mouseleave', '#rel-container .flow-commit', (e) => {
-    const rev = $(e.currentTarget).data('rev');
-    $(`#rev-list li#commit-${rev}`).removeClass('hover');
+
+  graphContainer.addEventListener('mouseleave', (e) => {
+    if (e.target.matches('#rev-list li')) {
+      const flow = e.target.getAttribute('data-flow');
+      if (flow === '0') return;
+      document.getElementById(`flow-${flow}`)?.classList.remove('highlight');
+      e.target.classList.remove('hover');
+      for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
+        item.classList.remove('highlight');
+      }
+    } else if (e.target.matches('#rel-container .flow-group')) {
+      e.target.classList.remove('highlight');
+      const flow = e.target.getAttribute('data-flow');
+      for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
+        item.classList.remove('highlight');
+      }
+    } else if (e.target.matches('#rel-container .flow-commit')) {
+      const rev = e.target.getAttribute('data-rev');
+      document.querySelector(`#rev-list li#commit-${rev}`)?.classList.remove('hover');
+    }
   });
 }
diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js
index 3603fae2e9..6a5bce8268 100644
--- a/web_src/js/features/repo-home.js
+++ b/web_src/js/features/repo-home.js
@@ -1,82 +1,79 @@
 import $ from 'jquery';
 import {stripTags} from '../utils.js';
-import {hideElem, showElem} from '../utils/dom.js';
+import {hideElem, queryElemChildren, showElem} from '../utils/dom.js';
+import {POST} from '../modules/fetch.js';
+import {showErrorToast} from '../modules/toast.js';
 
-const {appSubUrl, csrfToken} = window.config;
+const {appSubUrl} = window.config;
 
 export function initRepoTopicBar() {
-  const mgrBtn = $('#manage_topic');
-  if (!mgrBtn.length) return;
-  const editDiv = $('#topic_edit');
-  const viewDiv = $('#repo-topics');
-  const saveBtn = $('#save_topic');
-  const topicDropdown = $('#topic_edit .dropdown');
-  const topicForm = editDiv; // the old logic, editDiv is topicForm
-  const topicDropdownSearch = topicDropdown.find('input.search');
-  const topicPrompts = {
-    countPrompt: topicDropdown.attr('data-text-count-prompt'),
-    formatPrompt: topicDropdown.attr('data-text-format-prompt'),
-  };
+  const mgrBtn = document.getElementById('manage_topic');
+  if (!mgrBtn) return;
 
-  mgrBtn.on('click', () => {
+  const editDiv = document.getElementById('topic_edit');
+  const viewDiv = document.getElementById('repo-topics');
+  const topicDropdown = editDiv.querySelector('.ui.dropdown');
+  let lastErrorToast;
+
+  mgrBtn.addEventListener('click', () => {
     hideElem(viewDiv);
     showElem(editDiv);
-    topicDropdownSearch.focus();
+    topicDropdown.querySelector('input.search').focus();
   });
 
-  $('#cancel_topic_edit').on('click', () => {
+  document.querySelector('#cancel_topic_edit').addEventListener('click', () => {
+    lastErrorToast?.hideToast();
     hideElem(editDiv);
     showElem(viewDiv);
     mgrBtn.focus();
   });
 
-  saveBtn.on('click', () => {
-    const topics = $('input[name=topics]').val();
+  document.getElementById('save_topic').addEventListener('click', async (e) => {
+    lastErrorToast?.hideToast();
+    const topics = editDiv.querySelector('input[name=topics]').value;
 
-    $.post(saveBtn.attr('data-link'), {
-      _csrf: csrfToken,
-      topics
-    }, (_data, _textStatus, xhr) => {
-      if (xhr.responseJSON.status === 'ok') {
-        viewDiv.children('.topic').remove();
+    const data = new FormData();
+    data.append('topics', topics);
+
+    const response = await POST(e.target.getAttribute('data-link'), {data});
+
+    if (response.ok) {
+      const responseData = await response.json();
+      if (responseData.status === 'ok') {
+        queryElemChildren(viewDiv, '.repo-topic', (el) => el.remove());
         if (topics.length) {
           const topicArray = topics.split(',');
           topicArray.sort();
-          for (let i = 0; i < topicArray.length; i++) {
-            const link = $('<a class="ui repo-topic large label topic gt-m-0"></a>');
-            link.attr('href', `${appSubUrl}/explore/repos?q=${encodeURIComponent(topicArray[i])}&topic=1`);
-            link.text(topicArray[i]);
-            link.insertBefore(mgrBtn); // insert all new topics before manage button
+          for (const topic of topicArray) {
+            // it should match the code in repo/home.tmpl
+            const link = document.createElement('a');
+            link.classList.add('repo-topic', 'ui', 'large', 'label');
+            link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`;
+            link.textContent = topic;
+            mgrBtn.parentNode.insertBefore(link, mgrBtn); // insert all new topics before manage button
           }
         }
         hideElem(editDiv);
         showElem(viewDiv);
       }
-    }).fail((xhr) => {
-      if (xhr.status === 422) {
-        if (xhr.responseJSON.invalidTopics.length > 0) {
-          topicPrompts.formatPrompt = xhr.responseJSON.message;
-
-          const {invalidTopics} = xhr.responseJSON;
-          const topicLabels = topicDropdown.children('a.ui.label');
-
-          for (const [index, value] of topics.split(',').entries()) {
-            for (let i = 0; i < invalidTopics.length; i++) {
-              if (invalidTopics[i] === value) {
-                topicLabels.eq(index).removeClass('green').addClass('red');
-              }
-            }
+    } else if (response.status === 422) {
+      // how to test: input topic like " invalid topic " (with spaces), and select it from the list, then "Save"
+      const responseData = await response.json();
+      lastErrorToast = showErrorToast(responseData.message, {duration: 5000});
+      if (responseData.invalidTopics.length > 0) {
+        const {invalidTopics} = responseData;
+        const topicLabels = queryElemChildren(topicDropdown, 'a.ui.label');
+        for (const [index, value] of topics.split(',').entries()) {
+          if (invalidTopics.includes(value)) {
+            topicLabels[index].classList.remove('green');
+            topicLabels[index].classList.add('red');
           }
-        } else {
-          topicPrompts.countPrompt = xhr.responseJSON.message;
         }
       }
-    }).always(() => {
-      topicForm.form('validate form');
-    });
+    }
   });
 
-  topicDropdown.dropdown({
+  $(topicDropdown).dropdown({
     allowAdditions: true,
     forceSelection: false,
     fullTextSearch: 'exact',
@@ -99,9 +96,9 @@ export function initRepoTopicBar() {
         const query = stripTags(this.urlData.query.trim());
         let found_query = false;
         const current_topics = [];
-        topicDropdown.find('a.label.visible').each((_, el) => {
+        for (const el of queryElemChildren(topicDropdown, 'a.ui.label.visible')) {
           current_topics.push(el.getAttribute('data-value'));
-        });
+        }
 
         if (res.topics) {
           let found = false;
@@ -143,38 +140,8 @@ export function initRepoTopicBar() {
     },
     onAdd(addedValue, _addedText, $addedChoice) {
       addedValue = addedValue.toLowerCase().trim();
-      $($addedChoice).attr('data-value', addedValue);
-      $($addedChoice).attr('data-text', addedValue);
-    }
-  });
-
-  $.fn.form.settings.rules.validateTopic = function (_values, regExp) {
-    const topics = topicDropdown.children('a.ui.label');
-    const status = topics.length === 0 || topics.last().attr('data-value').match(regExp);
-    if (!status) {
-      topics.last().removeClass('green').addClass('red');
-    }
-    return status && topicDropdown.children('a.ui.label.red').length === 0;
-  };
-
-  topicForm.form({
-    on: 'change',
-    inline: true,
-    fields: {
-      topics: {
-        identifier: 'topics',
-        rules: [
-          {
-            type: 'validateTopic',
-            value: /^\s*[a-z0-9][-.a-z0-9]{0,35}\s*$/,
-            prompt: topicPrompts.formatPrompt
-          },
-          {
-            type: 'maxCount[25]',
-            prompt: topicPrompts.countPrompt
-          }
-        ]
-      },
-    }
+      $addedChoice[0].setAttribute('data-value', addedValue);
+      $addedChoice[0].setAttribute('data-text', addedValue);
+    },
   });
 }
diff --git a/web_src/js/features/repo-issue-content.js b/web_src/js/features/repo-issue-content.js
index 7832641687..cef2f49008 100644
--- a/web_src/js/features/repo-issue-content.js
+++ b/web_src/js/features/repo-issue-content.js
@@ -1,8 +1,10 @@
 import $ from 'jquery';
 import {svg} from '../svg.js';
 import {showErrorToast} from '../modules/toast.js';
+import {GET, POST} from '../modules/fetch.js';
+import {showElem} from '../utils/dom.js';
 
-const {appSubUrl, csrfToken} = window.config;
+const {appSubUrl} = window.config;
 let i18nTextEdited;
 let i18nTextOptions;
 let i18nTextDeleteFromHistory;
@@ -15,9 +17,9 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
   $dialog = $(`
 <div class="ui modal content-history-detail-dialog">
   ${svg('octicon-x', 16, 'close icon inside')}
-  <div class="header gt-df gt-ac gt-sb">
+  <div class="header tw-flex tw-items-center tw-justify-between">
     <div>${itemTitleHtml}</div>
-    <div class="ui dropdown dialog-header-options gt-mr-5 gt-hidden">
+    <div class="ui dropdown dialog-header-options tw-mr-8 tw-hidden">
       ${i18nTextOptions}
       ${svg('octicon-triangle-down', 14, 'dropdown icon')}
       <div class="menu">
@@ -31,19 +33,27 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
   $dialog.find('.dialog-header-options').dropdown({
     showOnFocus: false,
     allowReselection: true,
-    onChange(_value, _text, $item) {
+    async onChange(_value, _text, $item) {
       const optionItem = $item.data('option-item');
       if (optionItem === 'delete') {
         if (window.confirm(i18nTextDeleteFromHistoryConfirm)) {
-          $.post(`${issueBaseUrl}/content-history/soft-delete?comment_id=${commentId}&history_id=${historyId}`, {
-            _csrf: csrfToken,
-          }).done((resp) => {
+          try {
+            const params = new URLSearchParams();
+            params.append('comment_id', commentId);
+            params.append('history_id', historyId);
+
+            const response = await POST(`${issueBaseUrl}/content-history/soft-delete?${params.toString()}`);
+            const resp = await response.json();
+
             if (resp.ok) {
               $dialog.modal('hide');
             } else {
               showErrorToast(resp.message);
             }
-          });
+          } catch (error) {
+            console.error('Error:', error);
+            showErrorToast('An error occurred while deleting the history.');
+          }
         }
       } else { // required by eslint
         showErrorToast(`unknown option item: ${optionItem}`);
@@ -51,22 +61,29 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
     },
     onHide() {
       $(this).dropdown('clear', true);
-    }
+    },
   });
   $dialog.modal({
-    onShow() {
-      $.ajax({
-        url: `${issueBaseUrl}/content-history/detail?comment_id=${commentId}&history_id=${historyId}`,
-        data: {
-          _csrf: csrfToken,
-        },
-      }).done((resp) => {
-        $dialog.find('.comment-diff-data').removeClass('is-loading').html(resp.diffHtml);
+    async onShow() {
+      try {
+        const params = new URLSearchParams();
+        params.append('comment_id', commentId);
+        params.append('history_id', historyId);
+
+        const url = `${issueBaseUrl}/content-history/detail?${params.toString()}`;
+        const response = await GET(url);
+        const resp = await response.json();
+
+        const commentDiffData = $dialog.find('.comment-diff-data')[0];
+        commentDiffData?.classList.remove('is-loading');
+        commentDiffData.innerHTML = resp.diffHtml;
         // there is only one option "item[data-option-item=delete]", so the dropdown can be entirely shown/hidden.
         if (resp.canSoftDelete) {
-          $dialog.find('.dialog-header-options').removeClass('gt-hidden');
+          showElem($dialog.find('.dialog-header-options'));
         }
-      });
+      } catch (error) {
+        console.error('Error:', error);
+      }
     },
     onHidden() {
       $dialog.remove();
@@ -103,7 +120,7 @@ function showContentHistoryMenu(issueBaseUrl, $item, commentId) {
   });
 }
 
-export function initRepoIssueContentHistory() {
+export async function initRepoIssueContentHistory() {
   const issueIndex = $('#issueIndex').val();
   if (!issueIndex) return;
 
@@ -114,12 +131,10 @@ export function initRepoIssueContentHistory() {
   const repoLink = $('#repolink').val();
   const issueBaseUrl = `${appSubUrl}/${repoLink}/issues/${issueIndex}`;
 
-  $.ajax({
-    url: `${issueBaseUrl}/content-history/overview`,
-    data: {
-      _csrf: csrfToken,
-    },
-  }).done((resp) => {
+  try {
+    const response = await GET(`${issueBaseUrl}/content-history/overview`);
+    const resp = await response.json();
+
     i18nTextEdited = resp.i18n.textEdited;
     i18nTextDeleteFromHistory = resp.i18n.textDeleteFromHistory;
     i18nTextDeleteFromHistoryConfirm = resp.i18n.textDeleteFromHistoryConfirm;
@@ -133,5 +148,7 @@ export function initRepoIssueContentHistory() {
       const $itemComment = $(`#issuecomment-${commentId}`);
       showContentHistoryMenu(issueBaseUrl, $itemComment, commentId);
     }
-  });
+  } catch (error) {
+    console.error('Error:', error);
+  }
 }
diff --git a/web_src/js/features/repo-issue-edit.js b/web_src/js/features/repo-issue-edit.js
new file mode 100644
index 0000000000..4c03325c7a
--- /dev/null
+++ b/web_src/js/features/repo-issue-edit.js
@@ -0,0 +1,206 @@
+import $ from 'jquery';
+import {handleReply} from './repo-issue.js';
+import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
+import {createDropzone} from './dropzone.js';
+import {GET, POST} from '../modules/fetch.js';
+import {hideElem, showElem} from '../utils/dom.js';
+import {attachRefIssueContextPopup} from './contextpopup.js';
+import {initCommentContent, initMarkupContent} from '../markup/content.js';
+
+const {csrfToken} = window.config;
+
+async function onEditContent(event) {
+  event.preventDefault();
+
+  const segment = this.closest('.header').nextElementSibling;
+  const editContentZone = segment.querySelector('.edit-content-zone');
+  const renderContent = segment.querySelector('.render-content');
+  const rawContent = segment.querySelector('.raw-content');
+
+  let comboMarkdownEditor;
+
+  /**
+   * @param {HTMLElement} dropzone
+   */
+  const setupDropzone = async (dropzone) => {
+    if (!dropzone) return null;
+
+    let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
+    let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
+    const dz = await createDropzone(dropzone, {
+      url: dropzone.getAttribute('data-upload-url'),
+      headers: {'X-Csrf-Token': csrfToken},
+      maxFiles: dropzone.getAttribute('data-max-file'),
+      maxFilesize: dropzone.getAttribute('data-max-size'),
+      acceptedFiles: ['*/*', ''].includes(dropzone.getAttribute('data-accepts')) ? null : dropzone.getAttribute('data-accepts'),
+      addRemoveLinks: true,
+      dictDefaultMessage: dropzone.getAttribute('data-default-message'),
+      dictInvalidFileType: dropzone.getAttribute('data-invalid-input-type'),
+      dictFileTooBig: dropzone.getAttribute('data-file-too-big'),
+      dictRemoveFile: dropzone.getAttribute('data-remove-file'),
+      timeout: 0,
+      thumbnailMethod: 'contain',
+      thumbnailWidth: 480,
+      thumbnailHeight: 480,
+      init() {
+        this.on('success', (file, data) => {
+          file.uuid = data.uuid;
+          fileUuidDict[file.uuid] = {submitted: false};
+          const input = document.createElement('input');
+          input.id = data.uuid;
+          input.name = 'files';
+          input.type = 'hidden';
+          input.value = data.uuid;
+          dropzone.querySelector('.files').append(input);
+        });
+        this.on('removedfile', async (file) => {
+          document.getElementById(file.uuid)?.remove();
+          if (disableRemovedfileEvent) return;
+          if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
+            try {
+              await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
+            } catch (error) {
+              console.error(error);
+            }
+          }
+        });
+        this.on('submit', () => {
+          for (const fileUuid of Object.keys(fileUuidDict)) {
+            fileUuidDict[fileUuid].submitted = true;
+          }
+        });
+        this.on('reload', async () => {
+          try {
+            const response = await GET(editContentZone.getAttribute('data-attachment-url'));
+            const data = await response.json();
+            // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
+            disableRemovedfileEvent = true;
+            dz.removeAllFiles(true);
+            dropzone.querySelector('.files').innerHTML = '';
+            for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove();
+            fileUuidDict = {};
+            disableRemovedfileEvent = false;
+
+            for (const attachment of data) {
+              const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`;
+              dz.emit('addedfile', attachment);
+              dz.emit('thumbnail', attachment, imgSrc);
+              dz.emit('complete', attachment);
+              fileUuidDict[attachment.uuid] = {submitted: true};
+              dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%';
+              const input = document.createElement('input');
+              input.id = attachment.uuid;
+              input.name = 'files';
+              input.type = 'hidden';
+              input.value = attachment.uuid;
+              dropzone.querySelector('.files').append(input);
+            }
+            if (!dropzone.querySelector('.dz-preview')) {
+              dropzone.classList.remove('dz-started');
+            }
+          } catch (error) {
+            console.error(error);
+          }
+        });
+      },
+    });
+    dz.emit('reload');
+    return dz;
+  };
+
+  const cancelAndReset = (e) => {
+    e.preventDefault();
+    showElem(renderContent);
+    hideElem(editContentZone);
+    comboMarkdownEditor.attachedDropzoneInst?.emit('reload');
+  };
+
+  const saveAndRefresh = async (e) => {
+    e.preventDefault();
+    showElem(renderContent);
+    hideElem(editContentZone);
+    const dropzoneInst = comboMarkdownEditor.attachedDropzoneInst;
+    try {
+      const params = new URLSearchParams({
+        content: comboMarkdownEditor.value(),
+        context: editContentZone.getAttribute('data-context'),
+      });
+      for (const fileInput of dropzoneInst?.element.querySelectorAll('.files [name=files]')) params.append('files[]', fileInput.value);
+
+      const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
+      const data = await response.json();
+      if (!data.content) {
+        renderContent.innerHTML = document.getElementById('no-content').innerHTML;
+        rawContent.textContent = '';
+      } else {
+        renderContent.innerHTML = data.content;
+        rawContent.textContent = comboMarkdownEditor.value();
+        const refIssues = renderContent.querySelectorAll('p .ref-issue');
+        attachRefIssueContextPopup(refIssues);
+      }
+      const content = segment;
+      if (!content.querySelector('.dropzone-attachments')) {
+        if (data.attachments !== '') {
+          content.insertAdjacentHTML('beforeend', data.attachments);
+        }
+      } else if (data.attachments === '') {
+        content.querySelector('.dropzone-attachments').remove();
+      } else {
+        content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
+      }
+      dropzoneInst?.emit('submit');
+      dropzoneInst?.emit('reload');
+      initMarkupContent();
+      initCommentContent();
+    } catch (error) {
+      console.error(error);
+    }
+  };
+
+  comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
+  if (!comboMarkdownEditor) {
+    editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML;
+    comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
+    comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone'));
+    editContentZone.querySelector('.cancel.button').addEventListener('click', cancelAndReset);
+    editContentZone.querySelector('.save.button').addEventListener('click', saveAndRefresh);
+  }
+
+  // Show write/preview tab and copy raw content as needed
+  showElem(editContentZone);
+  hideElem(renderContent);
+  if (!comboMarkdownEditor.value()) {
+    comboMarkdownEditor.value(rawContent.textContent);
+  }
+  comboMarkdownEditor.focus();
+}
+
+export function initRepoIssueCommentEdit() {
+  // Edit issue or comment content
+  $(document).on('click', '.edit-content', onEditContent);
+
+  // Quote reply
+  $(document).on('click', '.quote-reply', async function (event) {
+    event.preventDefault();
+    const target = $(this).data('target');
+    const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
+    const content = `> ${quote}\n\n`;
+    let editor;
+    if ($(this).hasClass('quote-reply-diff')) {
+      const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
+      editor = await handleReply($replyBtn);
+    } else {
+      // for normal issue/comment page
+      editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
+    }
+    if (editor) {
+      if (editor.value()) {
+        editor.value(`${editor.value()}\n\n${content}`);
+      } else {
+        editor.value(content);
+      }
+      editor.focus();
+      editor.moveCursorToEnd();
+    }
+  });
+}
diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index ca20cfbe38..92f058c4d2 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -1,39 +1,51 @@
 import $ from 'jquery';
 import {updateIssuesMeta} from './repo-issue.js';
-import {toggleElem, hideElem} from '../utils/dom.js';
+import {toggleElem, hideElem, isElemHidden} from '../utils/dom.js';
 import {htmlEscape} from 'escape-goat';
 import {confirmModal} from './comp/ConfirmModal.js';
 import {showErrorToast} from '../modules/toast.js';
 import {createSortable} from '../modules/sortable.js';
 import {DELETE, POST} from '../modules/fetch.js';
+import {parseDom} from '../utils.js';
 
 function initRepoIssueListCheckboxes() {
-  const $issueSelectAll = $('.issue-checkbox-all');
-  const $issueCheckboxes = $('.issue-checkbox');
+  const issueSelectAll = document.querySelector('.issue-checkbox-all');
+  if (!issueSelectAll) return; // logged out state
+  const issueCheckboxes = document.querySelectorAll('.issue-checkbox');
 
   const syncIssueSelectionState = () => {
-    const $checked = $issueCheckboxes.filter(':checked');
-    const anyChecked = $checked.length !== 0;
-    const allChecked = anyChecked && $checked.length === $issueCheckboxes.length;
+    const checkedCheckboxes = Array.from(issueCheckboxes).filter((el) => el.checked);
+    const anyChecked = Boolean(checkedCheckboxes.length);
+    const allChecked = anyChecked && checkedCheckboxes.length === issueCheckboxes.length;
 
     if (allChecked) {
-      $issueSelectAll.prop({'checked': true, 'indeterminate': false});
+      issueSelectAll.checked = true;
+      issueSelectAll.indeterminate = false;
     } else if (anyChecked) {
-      $issueSelectAll.prop({'checked': false, 'indeterminate': true});
+      issueSelectAll.checked = false;
+      issueSelectAll.indeterminate = true;
     } else {
-      $issueSelectAll.prop({'checked': false, 'indeterminate': false});
+      issueSelectAll.checked = false;
+      issueSelectAll.indeterminate = false;
     }
     // if any issue is selected, show the action panel, otherwise show the filter panel
     toggleElem($('#issue-filters'), !anyChecked);
     toggleElem($('#issue-actions'), anyChecked);
     // there are two panels but only one select-all checkbox, so move the checkbox to the visible panel
-    $('#issue-filters, #issue-actions').filter(':visible').find('.issue-list-toolbar-left').prepend($issueSelectAll);
+    const panels = document.querySelectorAll('#issue-filters, #issue-actions');
+    const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el));
+    const toolbarLeft = visiblePanel.querySelector('.issue-list-toolbar-left');
+    toolbarLeft.prepend(issueSelectAll);
   };
 
-  $issueCheckboxes.on('change', syncIssueSelectionState);
+  for (const el of issueCheckboxes) {
+    el.addEventListener('change', syncIssueSelectionState);
+  }
 
-  $issueSelectAll.on('change', () => {
-    $issueCheckboxes.prop('checked', $issueSelectAll.is(':checked'));
+  issueSelectAll.addEventListener('change', () => {
+    for (const el of issueCheckboxes) {
+      el.checked = issueSelectAll.checked;
+    }
     syncIssueSelectionState();
   });
 
@@ -69,16 +81,12 @@ function initRepoIssueListCheckboxes() {
       }
     }
 
-    updateIssuesMeta(
-      url,
-      action,
-      issueIDs,
-      elementId,
-    ).then(() => {
+    try {
+      await updateIssuesMeta(url, action, issueIDs, elementId);
       window.location.reload();
-    }).catch((reason) => {
-      showErrorToast(reason.responseJSON.error);
-    });
+    } catch (err) {
+      showErrorToast(err.responseJSON?.error ?? err.message);
+    }
   });
 }
 
@@ -86,9 +94,9 @@ function initRepoIssueListAuthorDropdown() {
   const $searchDropdown = $('.user-remote-search');
   if (!$searchDropdown.length) return;
 
-  let searchUrl = $searchDropdown.attr('data-search-url');
-  const actionJumpUrl = $searchDropdown.attr('data-action-jump-url');
-  const selectedUserId = $searchDropdown.attr('data-selected-user-id');
+  let searchUrl = $searchDropdown[0].getAttribute('data-search-url');
+  const actionJumpUrl = $searchDropdown[0].getAttribute('data-action-jump-url');
+  const selectedUserId = $searchDropdown[0].getAttribute('data-selected-user-id');
   if (!searchUrl.includes('?')) searchUrl += '?';
 
   $searchDropdown.dropdown('setting', {
@@ -101,8 +109,8 @@ function initRepoIssueListAuthorDropdown() {
         // the content is provided by backend IssuePosters handler
         const processedResults = []; // to be used by dropdown to generate menu items
         for (const item of resp.results) {
-          let html = `<img class="ui avatar gt-vm" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`;
-          if (item.full_name) html += `<span class="search-fullname gt-ml-3">${htmlEscape(item.full_name)}</span>`;
+          let html = `<img class="ui avatar tw-align-middle" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`;
+          if (item.full_name) html += `<span class="search-fullname tw-ml-2">${htmlEscape(item.full_name)}</span>`;
           processedResults.push({value: item.user_id, name: html});
         }
         resp.results = processedResults;
@@ -122,20 +130,29 @@ function initRepoIssueListAuthorDropdown() {
   const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates');
   $searchDropdown.dropdown('internal', 'setup', dropdownSetup);
   dropdownSetup.menu = function (values) {
-    const $menu = $searchDropdown.find('> .menu');
-    $menu.find('> .dynamic-item').remove(); // remove old dynamic items
+    const menu = $searchDropdown.find('> .menu')[0];
+    // remove old dynamic items
+    for (const el of menu.querySelectorAll(':scope > .dynamic-item')) {
+      el.remove();
+    }
 
     const newMenuHtml = dropdownTemplates.menu(values, $searchDropdown.dropdown('setting', 'fields'), true /* html */, $searchDropdown.dropdown('setting', 'className'));
     if (newMenuHtml) {
-      const $newMenuItems = $(newMenuHtml);
-      $newMenuItems.addClass('dynamic-item');
-      $menu.append('<div class="divider dynamic-item"></div>', ...$newMenuItems);
+      const newMenuItems = parseDom(newMenuHtml, 'text/html').querySelectorAll('body > div');
+      for (const newMenuItem of newMenuItems) {
+        newMenuItem.classList.add('dynamic-item');
+      }
+      const div = document.createElement('div');
+      div.classList.add('divider', 'dynamic-item');
+      menu.append(div, ...newMenuItems);
     }
     $searchDropdown.dropdown('refresh');
     // defer our selection to the next tick, because dropdown will set the selection item after this `menu` function
     setTimeout(() => {
-      $menu.find('.item.active, .item.selected').removeClass('active selected');
-      $menu.find(`.item[data-value="${selectedUserId}"]`).addClass('selected');
+      for (const el of menu.querySelectorAll('.item.active, .item.selected')) {
+        el.classList.remove('active', 'selected');
+      }
+      menu.querySelector(`.item[data-value="${selectedUserId}"]`)?.classList.add('selected');
     }, 0);
   };
 }
@@ -179,8 +196,6 @@ async function initIssuePinSort() {
 
   createSortable(pinDiv, {
     group: 'shared',
-    animation: 150,
-    ghostClass: 'card-ghost',
     onEnd: pinMoveEnd,
   });
 }
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 6908e0c912..0d326aae58 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -5,8 +5,10 @@ import {hideElem, showElem, toggleElem} from '../utils/dom.js';
 import {setFileFolding} from './file-fold.js';
 import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
 import {toAbsoluteUrl} from '../utils.js';
+import {initDropzone} from './common-global.js';
+import {POST, GET} from '../modules/fetch.js';
 
-const {appSubUrl, csrfToken} = window.config;
+const {appSubUrl} = window.config;
 
 export function initRepoIssueTimeTracking() {
   $(document).on('click', '.issue-add-time', () => {
@@ -39,39 +41,37 @@ export function initRepoIssueTimeTracking() {
   });
 }
 
-function updateDeadline(deadlineString) {
-  hideElem($('#deadline-err-invalid-date'));
-  $('#deadline-loader').addClass('loading');
+async function updateDeadline(deadlineString) {
+  hideElem('#deadline-err-invalid-date');
+  document.getElementById('deadline-loader')?.classList.add('is-loading');
 
   let realDeadline = null;
   if (deadlineString !== '') {
     const newDate = Date.parse(deadlineString);
 
     if (Number.isNaN(newDate)) {
-      $('#deadline-loader').removeClass('loading');
-      showElem($('#deadline-err-invalid-date'));
+      document.getElementById('deadline-loader')?.classList.remove('is-loading');
+      showElem('#deadline-err-invalid-date');
       return false;
     }
     realDeadline = new Date(newDate);
   }
 
-  $.ajax(`${$('#update-issue-deadline-form').attr('action')}`, {
-    data: JSON.stringify({
-      due_date: realDeadline,
-    }),
-    headers: {
-      'X-Csrf-Token': csrfToken,
-    },
-    contentType: 'application/json',
-    type: 'POST',
-    success() {
+  try {
+    const response = await POST(document.getElementById('update-issue-deadline-form').getAttribute('action'), {
+      data: {due_date: realDeadline},
+    });
+
+    if (response.ok) {
       window.location.reload();
-    },
-    error() {
-      $('#deadline-loader').removeClass('loading');
-      showElem($('#deadline-err-invalid-date'));
-    },
-  });
+    } else {
+      throw new Error('Invalid response');
+    }
+  } catch (error) {
+    console.error(error);
+    document.getElementById('deadline-loader').classList.remove('is-loading');
+    showElem('#deadline-err-invalid-date');
+  }
 }
 
 export function initRepoIssueDue() {
@@ -87,6 +87,19 @@ export function initRepoIssueDue() {
   });
 }
 
+/**
+ * @param {HTMLElement} item
+ */
+function excludeLabel(item) {
+  const href = item.getAttribute('href');
+  const id = item.getAttribute('data-label-id');
+
+  const regStr = `labels=((?:-?[0-9]+%2c)*)(${id})((?:%2c-?[0-9]+)*)&`;
+  const newStr = 'labels=$1-$2$3&';
+
+  window.location = href.replace(new RegExp(regStr), newStr);
+}
+
 export function initRepoIssueSidebarList() {
   const repolink = $('#repolink').val();
   const repoId = $('#repoId').val();
@@ -123,16 +136,6 @@ export function initRepoIssueSidebarList() {
       fullTextSearch: true,
     });
 
-  function excludeLabel(item) {
-    const href = $(item).attr('href');
-    const id = $(item).data('label-id');
-
-    const regStr = `labels=((?:-?[0-9]+%2c)*)(${id})((?:%2c-?[0-9]+)*)&`;
-    const newStr = 'labels=$1-$2$3&';
-
-    window.location = href.replace(new RegExp(regStr), newStr);
-  }
-
   $('.menu a.label-filter-item').each(function () {
     $(this).on('click', function (e) {
       if (e.altKey) {
@@ -144,9 +147,9 @@ export function initRepoIssueSidebarList() {
 
   $('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
     if (e.altKey && e.keyCode === 13) {
-      const selectedItems = $('.menu .ui.dropdown.label-filter .menu .item.selected');
-      if (selectedItems.length > 0) {
-        excludeLabel($(selectedItems[0]));
+      const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
+      if (selectedItem) {
+        excludeLabel(selectedItem);
       }
     }
   });
@@ -155,39 +158,55 @@ export function initRepoIssueSidebarList() {
 
 export function initRepoIssueCommentDelete() {
   // Delete comment
-  $(document).on('click', '.delete-comment', function () {
-    const $this = $(this);
-    if (window.confirm($this.data('locale'))) {
-      $.post($this.data('url'), {
-        _csrf: csrfToken,
-      }).done(() => {
-        const $conversationHolder = $this.closest('.conversation-holder');
+  document.addEventListener('click', async (e) => {
+    if (!e.target.matches('.delete-comment')) return;
+    e.preventDefault();
+
+    const deleteButton = e.target;
+    if (window.confirm(deleteButton.getAttribute('data-locale'))) {
+      try {
+        const response = await POST(deleteButton.getAttribute('data-url'));
+        if (!response.ok) throw new Error('Failed to delete comment');
+
+        const conversationHolder = deleteButton.closest('.conversation-holder');
+        const parentTimelineItem = deleteButton.closest('.timeline-item');
+        const parentTimelineGroup = deleteButton.closest('.timeline-item-group');
 
         // Check if this was a pending comment.
-        if ($conversationHolder.find('.pending-label').length) {
-          const $counter = $('#review-box .review-comments-counter');
-          let num = parseInt($counter.attr('data-pending-comment-number')) - 1 || 0;
+        if (conversationHolder?.querySelector('.pending-label')) {
+          const counter = document.querySelector('#review-box .review-comments-counter');
+          let num = parseInt(counter?.getAttribute('data-pending-comment-number')) - 1 || 0;
           num = Math.max(num, 0);
-          $counter.attr('data-pending-comment-number', num);
-          $counter.text(num);
+          counter.setAttribute('data-pending-comment-number', num);
+          counter.textContent = String(num);
         }
 
-        $(`#${$this.data('comment-id')}`).remove();
-        if ($conversationHolder.length && !$conversationHolder.find('.comment').length) {
-          const path = $conversationHolder.data('path');
-          const side = $conversationHolder.data('side');
-          const idx = $conversationHolder.data('idx');
-          const lineType = $conversationHolder.closest('tr').data('line-type');
+        document.getElementById(deleteButton.getAttribute('data-comment-id'))?.remove();
+
+        if (conversationHolder && !conversationHolder.querySelector('.comment')) {
+          const path = conversationHolder.getAttribute('data-path');
+          const side = conversationHolder.getAttribute('data-side');
+          const idx = conversationHolder.getAttribute('data-idx');
+          const lineType = conversationHolder.closest('tr').getAttribute('data-line-type');
+
           if (lineType === 'same') {
-            $(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).removeClass('gt-invisible');
+            document.querySelector(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).classList.remove('tw-invisible');
           } else {
-            $(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).removeClass('gt-invisible');
+            document.querySelector(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).classList.remove('tw-invisible');
           }
-          $conversationHolder.remove();
+
+          conversationHolder.remove();
         }
-      });
+
+        // Check if there is no review content, move the time avatar upward to avoid overlapping the content below.
+        if (!parentTimelineGroup?.querySelector('.timeline-item.comment') && !parentTimelineItem?.querySelector('.conversation-holder')) {
+          const timelineAvatar = parentTimelineGroup?.querySelector('.timeline-avatar');
+          timelineAvatar?.classList.remove('timeline-avatar-offset');
+        }
+      } catch (error) {
+        console.error(error);
+      }
     }
-    return false;
   });
 }
 
@@ -211,46 +230,62 @@ export function initRepoIssueDependencyDelete() {
 
 export function initRepoIssueCodeCommentCancel() {
   // Cancel inline code comment
-  $(document).on('click', '.cancel-code-comment', (e) => {
-    const form = $(e.currentTarget).closest('form');
-    if (form.length > 0 && form.hasClass('comment-form')) {
-      form.addClass('gt-hidden');
-      showElem(form.closest('.comment-code-cloud').find('button.comment-form-reply'));
+  document.addEventListener('click', (e) => {
+    if (!e.target.matches('.cancel-code-comment')) return;
+
+    const form = e.target.closest('form');
+    if (form?.classList.contains('comment-form')) {
+      hideElem(form);
+      showElem(form.closest('.comment-code-cloud')?.querySelectorAll('button.comment-form-reply'));
     } else {
-      form.closest('.comment-code-cloud').remove();
+      form.closest('.comment-code-cloud')?.remove();
     }
   });
 }
 
 export function initRepoPullRequestUpdate() {
   // Pull Request update button
-  const $pullUpdateButton = $('.update-button > button');
-  $pullUpdateButton.on('click', function (e) {
+  const pullUpdateButton = document.querySelector('.update-button > button');
+  if (!pullUpdateButton) return;
+
+  pullUpdateButton.addEventListener('click', async function (e) {
     e.preventDefault();
-    const $this = $(this);
-    const redirect = $this.data('redirect');
-    $this.addClass('loading');
-    $.post($this.data('do'), {
-      _csrf: csrfToken
-    }).done((data) => {
-      if (data.redirect) {
-        window.location.href = data.redirect;
-      } else if (redirect) {
-        window.location.href = redirect;
-      } else {
-        window.location.reload();
-      }
-    });
+    const redirect = this.getAttribute('data-redirect');
+    this.classList.add('is-loading');
+    let response;
+    try {
+      response = await POST(this.getAttribute('data-do'));
+    } catch (error) {
+      console.error(error);
+    } finally {
+      this.classList.remove('is-loading');
+    }
+    let data;
+    try {
+      data = await response?.json(); // the response is probably not a JSON
+    } catch (error) {
+      console.error(error);
+    }
+    if (data?.redirect) {
+      window.location.href = data.redirect;
+    } else if (redirect) {
+      window.location.href = redirect;
+    } else {
+      window.location.reload();
+    }
   });
 
   $('.update-button > .dropdown').dropdown({
     onChange(_text, _value, $choice) {
-      const $url = $choice.data('do');
-      if ($url) {
-        $pullUpdateButton.find('.button-text').text($choice.text());
-        $pullUpdateButton.data('do', $url);
+      const url = $choice[0].getAttribute('data-do');
+      if (url) {
+        const buttonText = pullUpdateButton.querySelector('.button-text');
+        if (buttonText) {
+          buttonText.textContent = $choice.text();
+        }
+        pullUpdateButton.setAttribute('data-do', url);
       }
-    }
+    },
   });
 }
 
@@ -261,26 +296,26 @@ export function initRepoPullRequestMergeInstruction() {
 }
 
 export function initRepoPullRequestAllowMaintainerEdit() {
-  const $checkbox = $('#allow-edits-from-maintainers');
-  if (!$checkbox.length) return;
+  const wrapper = document.getElementById('allow-edits-from-maintainers');
+  if (!wrapper) return;
 
-  const promptError = $checkbox.attr('data-prompt-error');
-  $checkbox.checkbox({
-    'onChange': () => {
-      const checked = $checkbox.checkbox('is checked');
-      let url = $checkbox.attr('data-url');
-      url += '/set_allow_maintainer_edit';
-      $checkbox.checkbox('set disabled');
-      $.ajax({url, type: 'POST',
-        data: {_csrf: csrfToken, allow_maintainer_edit: checked},
-        error: () => {
-          showTemporaryTooltip($checkbox[0], promptError);
-        },
-        complete: () => {
-          $checkbox.checkbox('set enabled');
-        },
-      });
-    },
+  wrapper.querySelector('input[type="checkbox"]')?.addEventListener('change', async (e) => {
+    const checked = e.target.checked;
+    const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`;
+    wrapper.classList.add('is-loading');
+    e.target.disabled = true;
+    try {
+      const response = await POST(url, {data: {allow_maintainer_edit: checked}});
+      if (!response.ok) {
+        throw new Error('Failed to update maintainer edit permission');
+      }
+    } catch (error) {
+      console.error(error);
+      showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error'));
+    } finally {
+      wrapper.classList.remove('is-loading');
+      e.target.disabled = false;
+    }
   });
 }
 
@@ -294,7 +329,7 @@ export function initRepoIssueReferenceRepositorySearch() {
           $.each(response.data, (_r, repo) => {
             filteredResponse.results.push({
               name: htmlEscape(repo.repository.full_name),
-              value: repo.repository.full_name
+              value: repo.repository.full_name,
             });
           });
           return filteredResponse;
@@ -303,9 +338,11 @@ export function initRepoIssueReferenceRepositorySearch() {
       },
       onChange(_value, _text, $choice) {
         const $form = $choice.closest('form');
-        $form.attr('action', `${appSubUrl}/${_text}/issues/new`);
+        if (!$form.length) return;
+
+        $form[0].setAttribute('action', `${appSubUrl}/${_text}/issues/new`);
       },
-      fullTextSearch: true
+      fullTextSearch: true,
     });
 }
 
@@ -328,48 +365,41 @@ export function initRepoIssueWipTitle() {
   });
 }
 
-export async function updateIssuesMeta(url, action, issueIds, elementId) {
-  return $.ajax({
-    type: 'POST',
-    url,
-    data: {
-      _csrf: csrfToken,
-      action,
-      issue_ids: issueIds,
-      id: elementId,
-    },
-  });
+export async function updateIssuesMeta(url, action, issue_ids, id) {
+  try {
+    const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})});
+    if (!response.ok) {
+      throw new Error('Failed to update issues meta');
+    }
+  } catch (error) {
+    console.error(error);
+  }
 }
 
 export function initRepoIssueComments() {
-  if ($('.repository.view.issue .timeline').length === 0) return;
+  if (!$('.repository.view.issue .timeline').length) return;
 
-  $('.re-request-review').on('click', function (e) {
+  $('.re-request-review').on('click', async function (e) {
     e.preventDefault();
-    const url = $(this).data('update-url');
-    const issueId = $(this).data('issue-id');
-    const id = $(this).data('id');
-    const isChecked = $(this).hasClass('checked');
+    const url = this.getAttribute('data-update-url');
+    const issueId = this.getAttribute('data-issue-id');
+    const id = this.getAttribute('data-id');
+    const isChecked = this.classList.contains('checked');
 
-    updateIssuesMeta(
-      url,
-      isChecked ? 'detach' : 'attach',
-      issueId,
-      id,
-    ).then(() => window.location.reload());
+    await updateIssuesMeta(url, isChecked ? 'detach' : 'attach', issueId, id);
+    window.location.reload();
   });
 
-  $(document).on('click', (event) => {
-    const urlTarget = $(':target');
-    if (urlTarget.length === 0) return;
+  document.addEventListener('click', (e) => {
+    const urlTarget = document.querySelector(':target');
+    if (!urlTarget) return;
 
-    const urlTargetId = urlTarget.attr('id');
+    const urlTargetId = urlTarget.id;
     if (!urlTargetId) return;
+
     if (!/^(issue|pull)(comment)?-\d+$/.test(urlTargetId)) return;
 
-    const $target = $(event.target);
-
-    if ($target.closest(`#${urlTargetId}`).length === 0) {
+    if (!e.target.closest(`#${urlTargetId}`)) {
       const scrollPosition = $(window).scrollTop();
       window.location.hash = '';
       $(window).scrollTop(scrollPosition);
@@ -380,13 +410,18 @@ export function initRepoIssueComments() {
 
 export async function handleReply($el) {
   hideElem($el);
-  const form = $el.closest('.comment-code-cloud').find('.comment-form');
-  form.removeClass('gt-hidden');
+  const $form = $el.closest('.comment-code-cloud').find('.comment-form');
+  showElem($form);
 
-  const $textarea = form.find('textarea');
+  const $textarea = $form.find('textarea');
   let editor = getComboMarkdownEditor($textarea);
   if (!editor) {
-    editor = await initComboMarkdownEditor(form.find('.combo-markdown-editor'));
+    // FIXME: the initialization of the dropzone is not consistent.
+    // When the page is loaded, the dropzone is initialized by initGlobalDropzone, but the editor is not initialized.
+    // When the form is submitted and partially reload, none of them is initialized.
+    const dropzone = $form.find('.dropzone')[0];
+    if (!dropzone.dropzone) initDropzone(dropzone);
+    editor = await initComboMarkdownEditor($form.find('.combo-markdown-editor'));
   }
   editor.focus();
   return editor;
@@ -398,31 +433,34 @@ export function initRepoPullRequestReview() {
     if (window.history.scrollRestoration !== 'manual') {
       window.history.scrollRestoration = 'manual';
     }
-    const commentDiv = $(window.location.hash);
+    const commentDiv = document.querySelector(window.location.hash);
     if (commentDiv) {
       // get the name of the parent id
-      const groupID = commentDiv.closest('div[id^="code-comments-"]').attr('id');
+      const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id');
       if (groupID && groupID.startsWith('code-comments-')) {
         const id = groupID.slice(14);
         const ancestorDiffBox = commentDiv.closest('.diff-file-box');
         // on pages like conversation, there is no diff header
-        const diffHeader = ancestorDiffBox.find('.diff-file-header');
+        const diffHeader = ancestorDiffBox?.querySelector('.diff-file-header');
+
         // offset is for scrolling
         let offset = 30;
-        if (diffHeader[0]) {
-          offset += $('.diff-detail-box').outerHeight() + diffHeader.outerHeight();
+        if (diffHeader) {
+          offset += $('.diff-detail-box').outerHeight() + $(diffHeader).outerHeight();
         }
-        $(`#show-outdated-${id}`).addClass('gt-hidden');
-        $(`#code-comments-${id}`).removeClass('gt-hidden');
-        $(`#code-preview-${id}`).removeClass('gt-hidden');
-        $(`#hide-outdated-${id}`).removeClass('gt-hidden');
+
+        document.getElementById(`show-outdated-${id}`).classList.add('tw-hidden');
+        document.getElementById(`code-comments-${id}`).classList.remove('tw-hidden');
+        document.getElementById(`code-preview-${id}`).classList.remove('tw-hidden');
+        document.getElementById(`hide-outdated-${id}`).classList.remove('tw-hidden');
         // if the comment box is folded, expand it
-        if (ancestorDiffBox.attr('data-folded') && ancestorDiffBox.attr('data-folded') === 'true') {
-          setFileFolding(ancestorDiffBox[0], ancestorDiffBox.find('.fold-file')[0], false);
+        if (ancestorDiffBox.getAttribute('data-folded') === 'true') {
+          setFileFolding(ancestorDiffBox, ancestorDiffBox.querySelector('.fold-file'), false);
         }
+
         window.scrollTo({
-          top: commentDiv.offset().top - offset,
-          behavior: 'instant'
+          top: $(commentDiv).offset().top - offset,
+          behavior: 'instant',
         });
       }
     }
@@ -430,20 +468,20 @@ export function initRepoPullRequestReview() {
 
   $(document).on('click', '.show-outdated', function (e) {
     e.preventDefault();
-    const id = $(this).data('comment');
-    $(this).addClass('gt-hidden');
-    $(`#code-comments-${id}`).removeClass('gt-hidden');
-    $(`#code-preview-${id}`).removeClass('gt-hidden');
-    $(`#hide-outdated-${id}`).removeClass('gt-hidden');
+    const id = this.getAttribute('data-comment');
+    hideElem(this);
+    showElem(`#code-comments-${id}`);
+    showElem(`#code-preview-${id}`);
+    showElem(`#hide-outdated-${id}`);
   });
 
   $(document).on('click', '.hide-outdated', function (e) {
     e.preventDefault();
-    const id = $(this).data('comment');
-    $(this).addClass('gt-hidden');
-    $(`#code-comments-${id}`).addClass('gt-hidden');
-    $(`#code-preview-${id}`).addClass('gt-hidden');
-    $(`#show-outdated-${id}`).removeClass('gt-hidden');
+    const id = this.getAttribute('data-comment');
+    hideElem(this);
+    hideElem(`#code-comments-${id}`);
+    hideElem(`#code-preview-${id}`);
+    showElem(`#show-outdated-${id}`);
   });
 
   $(document).on('click', 'button.comment-form-reply', async function (e) {
@@ -457,9 +495,7 @@ export function initRepoPullRequestReview() {
   }
 
   // The following part is only for diff views
-  if ($('.repository.pull.diff').length === 0) {
-    return;
-  }
+  if (!$('.repository.pull.diff').length) return;
 
   const $reviewBtn = $('.js-btn-review');
   const $panel = $reviewBtn.parent().find('.review-box-panel');
@@ -482,19 +518,20 @@ export function initRepoPullRequestReview() {
   }
 
   $(document).on('click', '.add-code-comment', async function (e) {
-    if ($(e.target).hasClass('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745
+    if (e.target.classList.contains('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745
     e.preventDefault();
 
-    const isSplit = $(this).closest('.code-diff').hasClass('code-diff-split');
-    const side = $(this).data('side');
-    const idx = $(this).data('idx');
-    const path = $(this).closest('[data-path]').data('path');
-    const tr = $(this).closest('tr');
-    const lineType = tr.data('line-type');
+    const isSplit = this.closest('.code-diff')?.classList.contains('code-diff-split');
+    const side = this.getAttribute('data-side');
+    const idx = this.getAttribute('data-idx');
+    const path = this.closest('[data-path]')?.getAttribute('data-path');
+    const tr = this.closest('tr');
+    const lineType = tr.getAttribute('data-line-type');
 
-    let ntr = tr.next();
-    if (!ntr.hasClass('add-comment')) {
-      ntr = $(`
+    const ntr = tr.nextElementSibling;
+    let $ntr = $(ntr);
+    if (!ntr?.classList.contains('add-comment')) {
+      $ntr = $(`
         <tr class="add-comment" data-line-type="${lineType}">
           ${isSplit ? `
             <td class="add-comment-left" colspan="4"></td>
@@ -503,20 +540,26 @@ export function initRepoPullRequestReview() {
             <td class="add-comment-left add-comment-right" colspan="5"></td>
           `}
         </tr>`);
-      tr.after(ntr);
+      $(tr).after($ntr);
     }
 
-    const td = ntr.find(`.add-comment-${side}`);
-    const commentCloud = td.find('.comment-code-cloud');
-    if (commentCloud.length === 0 && !ntr.find('button[name="pending_review"]').length) {
-      const html = await $.get($(this).closest('[data-new-comment-url]').attr('data-new-comment-url'));
-      td.html(html);
-      td.find("input[name='line']").val(idx);
-      td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
-      td.find("input[name='path']").val(path);
+    const $td = $ntr.find(`.add-comment-${side}`);
+    const $commentCloud = $td.find('.comment-code-cloud');
+    if (!$commentCloud.length && !$ntr.find('button[name="pending_review"]').length) {
+      try {
+        const response = await GET(this.closest('[data-new-comment-url]')?.getAttribute('data-new-comment-url'));
+        const html = await response.text();
+        $td.html(html);
+        $td.find("input[name='line']").val(idx);
+        $td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
+        $td.find("input[name='path']").val(path);
 
-      const editor = await initComboMarkdownEditor(td.find('.combo-markdown-editor'));
-      editor.focus();
+        initDropzone($td.find('.dropzone')[0]);
+        const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor'));
+        editor.focus();
+      } catch (error) {
+        console.error(error);
+      }
     }
   });
 }
@@ -544,14 +587,38 @@ export function initRepoIssueWipToggle() {
     const title = toggleWip.getAttribute('data-title');
     const wipPrefix = toggleWip.getAttribute('data-wip-prefix');
     const updateUrl = toggleWip.getAttribute('data-update-url');
-    await $.post(updateUrl, {
-      _csrf: csrfToken,
-      title: title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`,
-    });
-    window.location.reload();
+
+    try {
+      const params = new URLSearchParams();
+      params.append('title', title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`);
+
+      const response = await POST(updateUrl, {data: params});
+      if (!response.ok) {
+        throw new Error('Failed to toggle WIP status');
+      }
+      window.location.reload();
+    } catch (error) {
+      console.error(error);
+    }
   });
 }
 
+async function pullrequest_targetbranch_change(update_url) {
+  const targetBranch = $('#pull-target-branch').data('branch');
+  const $branchTarget = $('#branch_target');
+  if (targetBranch === $branchTarget.text()) {
+    window.location.reload();
+    return false;
+  }
+  try {
+    await POST(update_url, {data: new URLSearchParams({target_branch: targetBranch})});
+  } catch (error) {
+    console.error(error);
+  } finally {
+    window.location.reload();
+  }
+}
+
 export function initRepoIssueTitleEdit() {
   // Edit issue title
   const $issueTitle = $('#issue-title');
@@ -559,13 +626,13 @@ export function initRepoIssueTitleEdit() {
 
   const editTitleToggle = function () {
     toggleElem($issueTitle);
-    toggleElem($('.not-in-edit'));
-    toggleElem($('#edit-title-input'));
-    toggleElem($('#pull-desc'));
-    toggleElem($('#pull-desc-edit'));
-    toggleElem($('.in-edit'));
-    toggleElem($('.new-issue-button'));
-    $('#issue-title-wrapper').toggleClass('edit-active');
+    toggleElem('.not-in-edit');
+    toggleElem('#edit-title-input');
+    toggleElem('#pull-desc');
+    toggleElem('#pull-desc-edit');
+    toggleElem('.in-edit');
+    toggleElem('.new-issue-button');
+    document.getElementById('issue-title-wrapper')?.classList.toggle('edit-active');
     $editInput[0].focus();
     $editInput[0].select();
     return false;
@@ -573,39 +640,27 @@ export function initRepoIssueTitleEdit() {
 
   $('#edit-title').on('click', editTitleToggle);
   $('#cancel-edit-title').on('click', editTitleToggle);
-  $('#save-edit-title').on('click', editTitleToggle).on('click', function () {
-    const pullrequest_targetbranch_change = function (update_url) {
-      const targetBranch = $('#pull-target-branch').data('branch');
-      const $branchTarget = $('#branch_target');
-      if (targetBranch === $branchTarget.text()) {
-        window.location.reload();
-        return false;
-      }
-      $.post(update_url, {
-        _csrf: csrfToken,
-        target_branch: targetBranch
-      }).always(() => {
-        window.location.reload();
-      });
-    };
-
-    const pullrequest_target_update_url = $(this).attr('data-target-update-url');
-    if ($editInput.val().length === 0 || $editInput.val() === $issueTitle.text()) {
+  $('#save-edit-title').on('click', editTitleToggle).on('click', async function () {
+    const pullrequest_target_update_url = this.getAttribute('data-target-update-url');
+    if (!$editInput.val().length || $editInput.val() === $issueTitle.text()) {
       $editInput.val($issueTitle.text());
-      pullrequest_targetbranch_change(pullrequest_target_update_url);
+      await pullrequest_targetbranch_change(pullrequest_target_update_url);
     } else {
-      $.post($(this).attr('data-update-url'), {
-        _csrf: csrfToken,
-        title: $editInput.val()
-      }, (data) => {
+      try {
+        const params = new URLSearchParams();
+        params.append('title', $editInput.val());
+        const response = await POST(this.getAttribute('data-update-url'), {data: params});
+        const data = await response.json();
         $editInput.val(data.title);
         $issueTitle.text(data.title);
         if (pullrequest_target_update_url) {
-          pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window
+          await pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window
         } else {
           window.location.reload();
         }
-      });
+      } catch (error) {
+        console.error(error);
+      }
     }
     return false;
   });
@@ -613,18 +668,18 @@ export function initRepoIssueTitleEdit() {
 
 export function initRepoIssueBranchSelect() {
   const changeBranchSelect = function () {
-    const selectionTextField = $('#pull-target-branch');
+    const $selectionTextField = $('#pull-target-branch');
 
-    const baseName = selectionTextField.data('basename');
+    const baseName = $selectionTextField.data('basename');
     const branchNameNew = $(this).data('branch');
-    const branchNameOld = selectionTextField.data('branch');
+    const branchNameOld = $selectionTextField.data('branch');
 
     // Replace branch name to keep translation from HTML template
-    selectionTextField.html(selectionTextField.html().replace(
+    $selectionTextField.html($selectionTextField.html().replace(
       `${baseName}:${branchNameOld}`,
-      `${baseName}:${branchNameNew}`
+      `${baseName}:${branchNameNew}`,
     ));
-    selectionTextField.data('branch', branchNameNew); // update branch name in setting
+    $selectionTextField.data('branch', branchNameNew); // update branch name in setting
   };
   $('#branch-select > .item').on('click', changeBranchSelect);
 }
@@ -634,10 +689,11 @@ export function initSingleCommentEditor($commentForm) {
   // * normal new issue/pr page, no status-button
   // * issue/pr view page, with comment form, has status-button
   const opts = {};
-  const $statusButton = $('#status-button');
-  if ($statusButton.length) {
+  const statusButton = document.getElementById('status-button');
+  if (statusButton) {
     opts.onContentChanged = (editor) => {
-      $statusButton.text($statusButton.attr(editor.value().trim() ? 'data-status-and-comment' : 'data-status'));
+      const statusText = statusButton.getAttribute(editor.value().trim() ? 'data-status-and-comment' : 'data-status');
+      statusButton.textContent = statusText;
     };
   }
   initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), opts);
@@ -656,7 +712,7 @@ export function initIssueTemplateCommentEditors($commentForm) {
     const editor = await initComboMarkdownEditor($markdownEditor, {
       onContentChanged: (editor) => {
         $formField.val(editor.value());
-      }
+      },
     });
 
     $formField.on('focus', async () => {
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 08fe21190a..e83de27e4c 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -3,7 +3,7 @@ import {
   initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
   initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
   initRepoIssueTitleEdit, initRepoIssueWipToggle,
-  initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor,
+  initRepoPullRequestUpdate, updateIssuesMeta, initIssueTemplateCommentEditors, initSingleCommentEditor,
 } from './repo-issue.js';
 import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
 import {svg} from '../svg.js';
@@ -15,22 +15,18 @@ import {
 import {initCitationFileCopyContent} from './citation.js';
 import {initCompLabelEdit} from './comp/LabelEdit.js';
 import {initRepoDiffConversationNav} from './repo-diff.js';
-import {createDropzone} from './dropzone.js';
-import {initCommentContent, initMarkupContent} from '../markup/content.js';
 import {initCompReactionSelector} from './comp/ReactionSelector.js';
 import {initRepoSettingBranches} from './repo-settings.js';
 import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
 import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js';
 import {hideElem, showElem} from '../utils/dom.js';
-import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
-import {attachRefIssueContextPopup} from './contextpopup.js';
-
-const {csrfToken} = window.config;
+import {POST} from '../modules/fetch.js';
+import {initRepoIssueCommentEdit} from './repo-issue-edit.js';
 
 // if there are draft comments, confirm before reloading, to avoid losing comments
 function reloadConfirmDraftComment() {
   const commentTextareas = [
-    document.querySelector('.edit-content-zone:not(.gt-hidden) textarea'),
+    document.querySelector('.edit-content-zone:not(.tw-hidden) textarea'),
     document.querySelector('#comment-form textarea'),
   ];
   for (const textarea of commentTextareas) {
@@ -49,9 +45,7 @@ function reloadConfirmDraftComment() {
 
 export function initRepoCommentForm() {
   const $commentForm = $('.comment.form');
-  if ($commentForm.length === 0) {
-    return;
-  }
+  if (!$commentForm.length) return;
 
   if ($commentForm.find('.field.combo-editor-dropzone').length) {
     // at the moment, if a form has multiple combo-markdown-editors, it must be an issue template form
@@ -65,7 +59,7 @@ export function initRepoCommentForm() {
     const $selectBranch = $('.ui.select-branch');
     const $branchMenu = $selectBranch.find('.reference-list-menu');
     const $isNewIssue = $branchMenu.hasClass('new-issue');
-    $branchMenu.find('.item:not(.no-select)').on('click', function () {
+    $branchMenu.find('.item:not(.no-select)').on('click', async function () {
       const selectedValue = $(this).data('id');
       const editMode = $('#editing_mode').val();
       $($(this).data('id-selector')).val(selectedValue);
@@ -75,8 +69,15 @@ export function initRepoCommentForm() {
       }
 
       if (editMode === 'true') {
-        const form = $('#update_issueref_form');
-        $.post(form.attr('action'), {_csrf: csrfToken, ref: selectedValue}, () => window.location.reload());
+        const form = document.getElementById('update_issueref_form');
+        const params = new URLSearchParams();
+        params.append('ref', selectedValue);
+        try {
+          await POST(form.getAttribute('action'), {data: params});
+          window.location.reload();
+        } catch (error) {
+          console.error(error);
+        }
       } else if (editMode === '') {
         $selectBranch.find('.ui .branch-name').text(selectedValue);
       }
@@ -131,26 +132,26 @@ export function initRepoCommentForm() {
 
       hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
 
-      const clickedItem = $(this);
-      const scope = $(this).attr('data-scope');
+      const clickedItem = this; // eslint-disable-line unicorn/no-this-assignment
+      const scope = this.getAttribute('data-scope');
 
       $(this).parent().find('.item').each(function () {
         if (scope) {
           // Enable only clicked item for scoped labels
-          if ($(this).attr('data-scope') !== scope) {
+          if (this.getAttribute('data-scope') !== scope) {
             return true;
           }
-          if (!$(this).is(clickedItem) && !$(this).hasClass('checked')) {
+          if (this !== clickedItem && !$(this).hasClass('checked')) {
             return true;
           }
-        } else if (!$(this).is(clickedItem)) {
+        } else if (this !== clickedItem) {
           // Toggle for other labels
           return true;
         }
 
         if ($(this).hasClass('checked')) {
           $(this).removeClass('checked');
-          $(this).find('.octicon-check').addClass('gt-invisible');
+          $(this).find('.octicon-check').addClass('tw-invisible');
           if (hasUpdateAction) {
             if (!($(this).data('id') in items)) {
               items[$(this).data('id')] = {
@@ -164,7 +165,7 @@ export function initRepoCommentForm() {
           }
         } else {
           $(this).addClass('checked');
-          $(this).find('.octicon-check').removeClass('gt-invisible');
+          $(this).find('.octicon-check').removeClass('tw-invisible');
           if (hasUpdateAction) {
             if (!($(this).data('id') in items)) {
               items[$(this).data('id')] = {
@@ -189,15 +190,15 @@ export function initRepoCommentForm() {
       $(this).parent().find('.item').each(function () {
         if ($(this).hasClass('checked')) {
           listIds.push($(this).data('id'));
-          $($(this).data('id-selector')).removeClass('gt-hidden');
+          $($(this).data('id-selector')).removeClass('tw-hidden');
         } else {
-          $($(this).data('id-selector')).addClass('gt-hidden');
+          $($(this).data('id-selector')).addClass('tw-hidden');
         }
       });
-      if (listIds.length === 0) {
-        $noSelect.removeClass('gt-hidden');
+      if (!listIds.length) {
+        $noSelect.removeClass('tw-hidden');
       } else {
-        $noSelect.addClass('gt-hidden');
+        $noSelect.addClass('tw-hidden');
       }
       $($(this).parent().data('id')).val(listIds.join(','));
       return false;
@@ -205,17 +206,20 @@ export function initRepoCommentForm() {
     $listMenu.find('.no-select.item').on('click', function (e) {
       e.preventDefault();
       if (hasUpdateAction) {
-        updateIssuesMeta(
-          $listMenu.data('update-url'),
-          'clear',
-          $listMenu.data('issue-id'),
-          '',
-        ).then(reloadConfirmDraftComment);
+        (async () => {
+          await updateIssuesMeta(
+            $listMenu.data('update-url'),
+            'clear',
+            $listMenu.data('issue-id'),
+            '',
+          );
+          reloadConfirmDraftComment();
+        })();
       }
 
       $(this).parent().find('.item').each(function () {
         $(this).removeClass('checked');
-        $(this).find('.octicon-check').addClass('gt-invisible');
+        $(this).find('.octicon-check').addClass('tw-invisible');
       });
 
       if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
@@ -223,9 +227,9 @@ export function initRepoCommentForm() {
       }
 
       $list.find('.item').each(function () {
-        $(this).addClass('gt-hidden');
+        $(this).addClass('tw-hidden');
       });
-      $noSelect.removeClass('gt-hidden');
+      $noSelect.removeClass('tw-hidden');
       $($(this).parent().data('id')).val('');
     });
   }
@@ -248,21 +252,24 @@ export function initRepoCommentForm() {
 
       $(this).addClass('selected active');
       if (hasUpdateAction) {
-        updateIssuesMeta(
-          $menu.data('update-url'),
-          '',
-          $menu.data('issue-id'),
-          $(this).data('id'),
-        ).then(reloadConfirmDraftComment);
+        (async () => {
+          await updateIssuesMeta(
+            $menu.data('update-url'),
+            '',
+            $menu.data('issue-id'),
+            $(this).data('id'),
+          );
+          reloadConfirmDraftComment();
+        })();
       }
 
       let icon = '';
       if (input_id === '#milestone_id') {
-        icon = svg('octicon-milestone', 18, 'gt-mr-3');
+        icon = svg('octicon-milestone', 18, 'tw-mr-2');
       } else if (input_id === '#project_id') {
-        icon = svg('octicon-project', 18, 'gt-mr-3');
+        icon = svg('octicon-project', 18, 'tw-mr-2');
       } else if (input_id === '#assignee_id') {
-        icon = `<img class="ui avatar image gt-mr-3" alt="avatar" src=${$(this).data('avatar')}>`;
+        icon = `<img class="ui avatar image tw-mr-2" alt="avatar" src=${$(this).data('avatar')}>`;
       }
 
       $list.find('.selected').html(`
@@ -272,7 +279,7 @@ export function initRepoCommentForm() {
         </a>
       `);
 
-      $(`.ui${select_id}.list .no-select`).addClass('gt-hidden');
+      $(`.ui${select_id}.list .no-select`).addClass('tw-hidden');
       $(input_id).val($(this).data('id'));
     });
     $menu.find('.no-select.item').on('click', function () {
@@ -281,16 +288,19 @@ export function initRepoCommentForm() {
       });
 
       if (hasUpdateAction) {
-        updateIssuesMeta(
-          $menu.data('update-url'),
-          '',
-          $menu.data('issue-id'),
-          $(this).data('id'),
-        ).then(reloadConfirmDraftComment);
+        (async () => {
+          await updateIssuesMeta(
+            $menu.data('update-url'),
+            '',
+            $menu.data('issue-id'),
+            $(this).data('id'),
+          );
+          reloadConfirmDraftComment();
+        })();
       }
 
       $list.find('.selected').html('');
-      $list.find('.no-select').removeClass('gt-hidden');
+      $list.find('.no-select').removeClass('tw-hidden');
       $(input_id).val('');
     });
   }
@@ -301,167 +311,8 @@ export function initRepoCommentForm() {
   selectItem('.select-assignee', '#assignee_id');
 }
 
-async function onEditContent(event) {
-  event.preventDefault();
-
-  const $segment = $(this).closest('.header').next();
-  const $editContentZone = $segment.find('.edit-content-zone');
-  const $renderContent = $segment.find('.render-content');
-  const $rawContent = $segment.find('.raw-content');
-
-  let comboMarkdownEditor;
-
-  const setupDropzone = async ($dropzone) => {
-    if ($dropzone.length === 0) return null;
-
-    let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
-    let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
-    const dz = await createDropzone($dropzone[0], {
-      url: $dropzone.attr('data-upload-url'),
-      headers: {'X-Csrf-Token': csrfToken},
-      maxFiles: $dropzone.attr('data-max-file'),
-      maxFilesize: $dropzone.attr('data-max-size'),
-      acceptedFiles: (['*/*', ''].includes($dropzone.attr('data-accepts'))) ? null : $dropzone.attr('data-accepts'),
-      addRemoveLinks: true,
-      dictDefaultMessage: $dropzone.attr('data-default-message'),
-      dictInvalidFileType: $dropzone.attr('data-invalid-input-type'),
-      dictFileTooBig: $dropzone.attr('data-file-too-big'),
-      dictRemoveFile: $dropzone.attr('data-remove-file'),
-      timeout: 0,
-      thumbnailMethod: 'contain',
-      thumbnailWidth: 480,
-      thumbnailHeight: 480,
-      init() {
-        this.on('success', (file, data) => {
-          file.uuid = data.uuid;
-          fileUuidDict[file.uuid] = {submitted: false};
-          const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
-          $dropzone.find('.files').append(input);
-        });
-        this.on('removedfile', (file) => {
-          if (disableRemovedfileEvent) return;
-          $(`#${file.uuid}`).remove();
-          if ($dropzone.attr('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
-            $.post($dropzone.attr('data-remove-url'), {
-              file: file.uuid,
-              _csrf: csrfToken,
-            });
-          }
-        });
-        this.on('submit', () => {
-          $.each(fileUuidDict, (fileUuid) => {
-            fileUuidDict[fileUuid].submitted = true;
-          });
-        });
-        this.on('reload', () => {
-          $.getJSON($editContentZone.attr('data-attachment-url'), (data) => {
-            // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
-            disableRemovedfileEvent = true;
-            dz.removeAllFiles(true);
-            $dropzone.find('.files').empty();
-            fileUuidDict = {};
-            disableRemovedfileEvent = false;
-
-            for (const attachment of data) {
-              const imgSrc = `${$dropzone.attr('data-link-url')}/${attachment.uuid}`;
-              dz.emit('addedfile', attachment);
-              dz.emit('thumbnail', attachment, imgSrc);
-              dz.emit('complete', attachment);
-              dz.files.push(attachment);
-              fileUuidDict[attachment.uuid] = {submitted: true};
-              $dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%');
-              const input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid);
-              $dropzone.find('.files').append(input);
-            }
-          });
-        });
-      },
-    });
-    dz.emit('reload');
-    return dz;
-  };
-
-  const cancelAndReset = (dz) => {
-    showElem($renderContent);
-    hideElem($editContentZone);
-    if (dz) {
-      dz.emit('reload');
-    }
-  };
-
-  const saveAndRefresh = (dz, $dropzone) => {
-    showElem($renderContent);
-    hideElem($editContentZone);
-    const $attachments = $dropzone.find('.files').find('[name=files]').map(function () {
-      return $(this).val();
-    }).get();
-    $.post($editContentZone.attr('data-update-url'), {
-      _csrf: csrfToken,
-      content: comboMarkdownEditor.value(),
-      context: $editContentZone.attr('data-context'),
-      files: $attachments,
-    }, (data) => {
-      if (!data.content) {
-        $renderContent.html($('#no-content').html());
-        $rawContent.text('');
-      } else {
-        $renderContent.html(data.content);
-        $rawContent.text(comboMarkdownEditor.value());
-
-        const refIssues = $renderContent.find('p .ref-issue');
-        attachRefIssueContextPopup(refIssues);
-      }
-      const $content = $segment;
-      if (!$content.find('.dropzone-attachments').length) {
-        if (data.attachments !== '') {
-          $content.append(`<div class="dropzone-attachments"></div>`);
-          $content.find('.dropzone-attachments').replaceWith(data.attachments);
-        }
-      } else if (data.attachments === '') {
-        $content.find('.dropzone-attachments').remove();
-      } else {
-        $content.find('.dropzone-attachments').replaceWith(data.attachments);
-      }
-      if (dz) {
-        dz.emit('submit');
-        dz.emit('reload');
-      }
-      initMarkupContent();
-      initCommentContent();
-    });
-  };
-
-  if (!$editContentZone.html()) {
-    $editContentZone.html($('#issue-comment-editor-template').html());
-    comboMarkdownEditor = await initComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
-
-    const $dropzone = $editContentZone.find('.dropzone');
-    const dz = await setupDropzone($dropzone);
-    $editContentZone.find('.cancel.button').on('click', (e) => {
-      e.preventDefault();
-      cancelAndReset(dz);
-    });
-    $editContentZone.find('.save.button').on('click', (e) => {
-      e.preventDefault();
-      saveAndRefresh(dz, $dropzone);
-    });
-  } else {
-    comboMarkdownEditor = getComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
-  }
-
-  // Show write/preview tab and copy raw content as needed
-  showElem($editContentZone);
-  hideElem($renderContent);
-  if (!comboMarkdownEditor.value()) {
-    comboMarkdownEditor.value($rawContent.text());
-  }
-  comboMarkdownEditor.focus();
-}
-
 export function initRepository() {
-  if ($('.page-content.repository').length === 0) {
-    return;
-  }
+  if (!$('.page-content.repository').length) return;
 
   initRepoBranchTagSelector('.js-branch-tag-selector');
 
@@ -510,7 +361,7 @@ export function initRepository() {
       const gitignores = $('input[name="gitignores"]').val();
       const license = $('input[name="license"]').val();
       if (gitignores || license) {
-        $('input[name="auto_init"]').prop('checked', true);
+        document.querySelector('input[name="auto_init"]').checked = true;
       }
     });
   }
@@ -563,33 +414,3 @@ export function initRepository() {
 
   initUnicodeEscapeButton();
 }
-
-function initRepoIssueCommentEdit() {
-  // Edit issue or comment content
-  $(document).on('click', '.edit-content', onEditContent);
-
-  // Quote reply
-  $(document).on('click', '.quote-reply', async function (event) {
-    event.preventDefault();
-    const target = $(this).data('target');
-    const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
-    const content = `> ${quote}\n\n`;
-    let editor;
-    if ($(this).hasClass('quote-reply-diff')) {
-      const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
-      editor = await handleReply($replyBtn);
-    } else {
-      // for normal issue/comment page
-      editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
-    }
-    if (editor) {
-      if (editor.value()) {
-        editor.value(`${editor.value()}\n\n${content}`);
-      } else {
-        editor.value(content);
-      }
-      editor.focus();
-      editor.moveCursorToEnd();
-    }
-  });
-}
diff --git a/web_src/js/features/repo-migrate.js b/web_src/js/features/repo-migrate.js
index cae28fdd1b..490e7df0e4 100644
--- a/web_src/js/features/repo-migrate.js
+++ b/web_src/js/features/repo-migrate.js
@@ -1,18 +1,17 @@
-import $ from 'jquery';
 import {hideElem, showElem} from '../utils/dom.js';
 import {GET, POST} from '../modules/fetch.js';
 
 const {appSubUrl} = window.config;
 
 export function initRepoMigrationStatusChecker() {
-  const $repoMigrating = $('#repo_migrating');
-  if (!$repoMigrating.length) return;
+  const repoMigrating = document.getElementById('repo_migrating');
+  if (!repoMigrating) return;
 
-  $('#repo_migrating_retry').on('click', doMigrationRetry);
+  document.getElementById('repo_migrating_retry').addEventListener('click', doMigrationRetry);
 
-  const task = $repoMigrating.attr('data-migrating-task-id');
+  const task = repoMigrating.getAttribute('data-migrating-task-id');
 
-  // returns true if the refresh still need to be called after a while
+  // returns true if the refresh still needs to be called after a while
   const refresh = async () => {
     const res = await GET(`${appSubUrl}/user/task/${task}`);
     if (res.status !== 200) return true; // continue to refresh if network error occurs
@@ -21,7 +20,7 @@ export function initRepoMigrationStatusChecker() {
 
     // for all status
     if (data.message) {
-      $('#repo_migrating_progress_message').text(data.message);
+      document.getElementById('repo_migrating_progress_message').textContent = data.message;
     }
 
     // TaskStatusFinished
@@ -37,7 +36,7 @@ export function initRepoMigrationStatusChecker() {
       showElem('#repo_migrating_retry');
       showElem('#repo_migrating_failed');
       showElem('#repo_migrating_failed_image');
-      $('#repo_migrating_failed_error').text(data.message);
+      document.getElementById('repo_migrating_failed_error').textContent = data.message;
       return false;
     }
 
@@ -59,6 +58,6 @@ export function initRepoMigrationStatusChecker() {
 }
 
 async function doMigrationRetry(e) {
-  await POST($(e.target).attr('data-migrating-task-retry-url'));
+  await POST(e.target.getAttribute('data-migrating-task-retry-url'));
   window.location.reload();
 }
diff --git a/web_src/js/features/repo-migration.js b/web_src/js/features/repo-migration.js
index 3bd0e6d72c..59e282e4e7 100644
--- a/web_src/js/features/repo-migration.js
+++ b/web_src/js/features/repo-migration.js
@@ -1,38 +1,42 @@
-import $ from 'jquery';
 import {hideElem, showElem, toggleElem} from '../utils/dom.js';
 
-const $service = $('#service_type');
-const $user = $('#auth_username');
-const $pass = $('#auth_password');
-const $token = $('#auth_token');
-const $mirror = $('#mirror');
-const $lfs = $('#lfs');
-const $lfsSettings = $('#lfs_settings');
-const $lfsEndpoint = $('#lfs_endpoint');
-const $items = $('#migrate_items').find('input[type=checkbox]');
+const service = document.getElementById('service_type');
+const user = document.getElementById('auth_username');
+const pass = document.getElementById('auth_password');
+const token = document.getElementById('auth_token');
+const mirror = document.getElementById('mirror');
+const lfs = document.getElementById('lfs');
+const lfsSettings = document.getElementById('lfs_settings');
+const lfsEndpoint = document.getElementById('lfs_endpoint');
+const items = document.querySelectorAll('#migrate_items input[type=checkbox]');
 
 export function initRepoMigration() {
   checkAuth();
   setLFSSettingsVisibility();
 
-  $user.on('input', () => {checkItems(false)});
-  $pass.on('input', () => {checkItems(false)});
-  $token.on('input', () => {checkItems(true)});
-  $mirror.on('change', () => {checkItems(true)});
-  $('#lfs_settings_show').on('click', () => { showElem($lfsEndpoint); return false });
-  $lfs.on('change', setLFSSettingsVisibility);
+  user?.addEventListener('input', () => {checkItems(false)});
+  pass?.addEventListener('input', () => {checkItems(false)});
+  token?.addEventListener('input', () => {checkItems(true)});
+  mirror?.addEventListener('change', () => {checkItems(true)});
+  document.getElementById('lfs_settings_show')?.addEventListener('click', (e) => {
+    e.preventDefault();
+    e.stopPropagation();
+    showElem(lfsEndpoint);
+  });
+  lfs?.addEventListener('change', setLFSSettingsVisibility);
 
-  const $cloneAddr = $('#clone_addr');
-  $cloneAddr.on('change', () => {
-    const $repoName = $('#repo_name');
-    if ($cloneAddr.val().length > 0 && $repoName.val().length === 0) { // Only modify if repo_name input is blank
-      $repoName.val($cloneAddr.val().match(/^(.*\/)?((.+?)(\.git)?)$/)[3]);
+  const cloneAddr = document.getElementById('clone_addr');
+  cloneAddr?.addEventListener('change', () => {
+    const repoName = document.getElementById('repo_name');
+    if (cloneAddr.value && !repoName?.value) { // Only modify if repo_name input is blank
+      repoName.value = cloneAddr.value.match(/^(.*\/)?((.+?)(\.git)?)$/)[3];
     }
   });
 }
 
 function checkAuth() {
-  const serviceType = $service.val();
+  if (!service) return;
+  const serviceType = Number(service.value);
 
   checkItems(serviceType !== 1);
 }
@@ -40,24 +44,26 @@ function checkAuth() {
 function checkItems(tokenAuth) {
   let enableItems;
   if (tokenAuth) {
-    enableItems = $token.val() !== '';
+    enableItems = token?.value !== '';
   } else {
-    enableItems = $user.val() !== '' || $pass.val() !== '';
+    enableItems = user?.value !== '' || pass?.value !== '';
   }
-  if (enableItems && $service.val() > 1) {
-    if ($mirror.is(':checked')) {
-      $items.not('[name="wiki"]').attr('disabled', true);
-      $items.filter('[name="wiki"]').attr('disabled', false);
+  if (enableItems && Number(service?.value) > 1) {
+    if (mirror?.checked) {
+      for (const item of items) {
+        item.disabled = item.name !== 'wiki';
+      }
       return;
     }
-    $items.attr('disabled', false);
+    for (const item of items) item.disabled = false;
   } else {
-    $items.attr('disabled', true);
+    for (const item of items) item.disabled = true;
   }
 }
 
 function setLFSSettingsVisibility() {
-  const visible = $lfs.is(':checked');
-  toggleElem($lfsSettings, visible);
-  hideElem($lfsEndpoint);
+  if (!lfs) return;
+  const visible = lfs.checked;
+  toggleElem(lfsSettings, visible);
+  hideElem(lfsEndpoint);
 }
diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js
index 5a2a7e72ef..a869c24c82 100644
--- a/web_src/js/features/repo-projects.js
+++ b/web_src/js/features/repo-projects.js
@@ -1,9 +1,8 @@
 import $ from 'jquery';
-import {useLightTextOnBackground} from '../utils/color.js';
-import tinycolor from 'tinycolor2';
+import {contrastColor} from '../utils/color.js';
 import {createSortable} from '../modules/sortable.js';
-
-const {csrfToken} = window.config;
+import {POST, DELETE, PUT} from '../modules/fetch.js';
+import tinycolor from 'tinycolor2';
 
 function updateIssueCount(cards) {
   const parent = cards.parentElement;
@@ -11,45 +10,42 @@ function updateIssueCount(cards) {
   parent.getElementsByClassName('project-column-issue-count')[0].textContent = cnt;
 }
 
-function createNewColumn(url, columnTitle, projectColorInput) {
-  $.ajax({
-    url,
-    data: JSON.stringify({title: columnTitle.val(), color: projectColorInput.val()}),
-    headers: {
-      'X-Csrf-Token': csrfToken,
-    },
-    contentType: 'application/json',
-    method: 'POST',
-  }).done(() => {
+async function createNewColumn(url, columnTitle, projectColorInput) {
+  try {
+    await POST(url, {
+      data: {
+        title: columnTitle.val(),
+        color: projectColorInput.val(),
+      },
+    });
+  } catch (error) {
+    console.error(error);
+  } finally {
     columnTitle.closest('form').removeClass('dirty');
     window.location.reload();
-  });
+  }
 }
 
-function moveIssue({item, from, to, oldIndex}) {
+async function moveIssue({item, from, to, oldIndex}) {
   const columnCards = to.getElementsByClassName('issue-card');
   updateIssueCount(from);
   updateIssueCount(to);
 
   const columnSorting = {
     issues: Array.from(columnCards, (card, i) => ({
-      issueID: parseInt($(card).attr('data-issue')),
+      issueID: parseInt(card.getAttribute('data-issue')),
       sorting: i,
     })),
   };
 
-  $.ajax({
-    url: `${to.getAttribute('data-url')}/move`,
-    data: JSON.stringify(columnSorting),
-    headers: {
-      'X-Csrf-Token': csrfToken,
-    },
-    contentType: 'application/json',
-    type: 'POST',
-    error: () => {
-      from.insertBefore(item, from.children[oldIndex]);
-    },
-  });
+  try {
+    await POST(`${to.getAttribute('data-url')}/move`, {
+      data: columnSorting,
+    });
+  } catch (error) {
+    console.error(error);
+    from.insertBefore(item, from.children[oldIndex]);
+  }
 }
 
 async function initRepoProjectSortable() {
@@ -62,25 +58,21 @@ async function initRepoProjectSortable() {
   createSortable(mainBoard, {
     group: 'project-column',
     draggable: '.project-column',
-    filter: '[data-id="0"]',
-    animation: 150,
-    ghostClass: 'card-ghost',
+    handle: '.project-column-header',
     delayOnTouchOnly: true,
     delay: 500,
-    onSort: () => {
+    onSort: async () => {
       boardColumns = mainBoard.getElementsByClassName('project-column');
       for (let i = 0; i < boardColumns.length; i++) {
         const column = boardColumns[i];
-        if (parseInt($(column).data('sorting')) !== i) {
-          $.ajax({
-            url: $(column).data('url'),
-            data: JSON.stringify({sorting: i, color: rgbToHex($(column).css('backgroundColor'))}),
-            headers: {
-              'X-Csrf-Token': csrfToken,
-            },
-            contentType: 'application/json',
-            method: 'PUT',
-          });
+        if (parseInt(column.getAttribute('data-sorting')) !== i) {
+          try {
+            const bgColor = column.style.backgroundColor; // will be rgb() string
+            const color = bgColor ? tinycolor(bgColor).toHexString() : '';
+            await PUT(column.getAttribute('data-url'), {data: {sorting: i, color}});
+          } catch (error) {
+            console.error(error);
+          }
         }
       }
     },
@@ -90,8 +82,6 @@ async function initRepoProjectSortable() {
     const boardCardList = boardColumn.getElementsByClassName('cards')[0];
     createSortable(boardCardList, {
       group: 'shared',
-      animation: 150,
-      ghostClass: 'card-ghost',
       onAdd: moveIssue,
       onUpdate: moveIssue,
       delayOnTouchOnly: true,
@@ -101,115 +91,96 @@ async function initRepoProjectSortable() {
 }
 
 export function initRepoProject() {
-  if (!$('.repository.projects').length) {
+  if (!document.querySelector('.repository.projects')) {
     return;
   }
 
   const _promise = initRepoProjectSortable();
 
-  $('.edit-project-column-modal').each(function () {
-    const projectHeader = $(this).closest('.project-column-header');
-    const projectTitleLabel = projectHeader.find('.project-column-title');
-    const projectTitleInput = $(this).find('.project-column-title-input');
-    const projectColorInput = $(this).find('#new_project_column_color');
-    const boardColumn = $(this).closest('.project-column');
-
-    if (boardColumn.css('backgroundColor')) {
-      setLabelColor(projectHeader, rgbToHex(boardColumn.css('backgroundColor')));
-    }
-
-    $(this).find('.edit-project-column-button').on('click', function (e) {
+  for (const modal of document.getElementsByClassName('edit-project-column-modal')) {
+    const projectHeader = modal.closest('.project-column-header');
+    const projectTitleLabel = projectHeader?.querySelector('.project-column-title-label');
+    const projectTitleInput = modal.querySelector('.project-column-title-input');
+    const projectColorInput = modal.querySelector('#new_project_column_color');
+    const boardColumn = modal.closest('.project-column');
+    modal.querySelector('.edit-project-column-button')?.addEventListener('click', async function (e) {
       e.preventDefault();
-
-      $.ajax({
-        url: $(this).data('url'),
-        data: JSON.stringify({title: projectTitleInput.val(), color: projectColorInput.val()}),
-        headers: {
-          'X-Csrf-Token': csrfToken,
-        },
-        contentType: 'application/json',
-        method: 'PUT',
-      }).done(() => {
-        projectTitleLabel.text(projectTitleInput.val());
-        projectTitleInput.closest('form').removeClass('dirty');
-        if (projectColorInput.val()) {
-          setLabelColor(projectHeader, projectColorInput.val());
+      try {
+        await PUT(this.getAttribute('data-url'), {
+          data: {
+            title: projectTitleInput?.value,
+            color: projectColorInput?.value,
+          },
+        });
+      } catch (error) {
+        console.error(error);
+      } finally {
+        projectTitleLabel.textContent = projectTitleInput?.value;
+        projectTitleInput.closest('form')?.classList.remove('dirty');
+        const dividers = boardColumn.querySelectorAll(':scope > .divider');
+        if (projectColorInput.value) {
+          const color = contrastColor(projectColorInput.value);
+          boardColumn.style.setProperty('background', projectColorInput.value, 'important');
+          boardColumn.style.setProperty('color', color, 'important');
+          for (const divider of dividers) {
+            divider.style.setProperty('color', color);
+          }
+        } else {
+          boardColumn.style.removeProperty('background');
+          boardColumn.style.removeProperty('color');
+          for (const divider of dividers) {
+            divider.style.removeProperty('color');
+          }
         }
-        boardColumn.attr('style', `background: ${projectColorInput.val()}!important`);
         $('.ui.modal').modal('hide');
-      });
+      }
     });
-  });
+  }
 
   $('.default-project-column-modal').each(function () {
-    const boardColumn = $(this).closest('.project-column');
-    const showButton = $(boardColumn).find('.default-project-column-show');
-    const commitButton = $(this).find('.actions > .ok.button');
+    const $boardColumn = $(this).closest('.project-column');
+    const $showButton = $($boardColumn).find('.default-project-column-show');
+    const $commitButton = $(this).find('.actions > .ok.button');
 
-    $(commitButton).on('click', (e) => {
+    $($commitButton).on('click', async (e) => {
       e.preventDefault();
 
-      $.ajax({
-        method: 'POST',
-        url: $(showButton).data('url'),
-        headers: {
-          'X-Csrf-Token': csrfToken,
-        },
-        contentType: 'application/json',
-      }).done(() => {
+      try {
+        await POST($($showButton).data('url'));
+      } catch (error) {
+        console.error(error);
+      } finally {
         window.location.reload();
-      });
+      }
     });
   });
 
   $('.show-delete-project-column-modal').each(function () {
-    const deleteColumnModal = $(`${$(this).attr('data-modal')}`);
-    const deleteColumnButton = deleteColumnModal.find('.actions > .ok.button');
-    const deleteUrl = $(this).attr('data-url');
+    const $deleteColumnModal = $(`${this.getAttribute('data-modal')}`);
+    const $deleteColumnButton = $deleteColumnModal.find('.actions > .ok.button');
+    const deleteUrl = this.getAttribute('data-url');
 
-    deleteColumnButton.on('click', (e) => {
+    $deleteColumnButton.on('click', async (e) => {
       e.preventDefault();
 
-      $.ajax({
-        url: deleteUrl,
-        headers: {
-          'X-Csrf-Token': csrfToken,
-        },
-        contentType: 'application/json',
-        method: 'DELETE',
-      }).done(() => {
+      try {
+        await DELETE(deleteUrl);
+      } catch (error) {
+        console.error(error);
+      } finally {
         window.location.reload();
-      });
+      }
     });
   });
 
   $('#new_project_column_submit').on('click', (e) => {
     e.preventDefault();
-    const columnTitle = $('#new_project_column');
-    const projectColorInput = $('#new_project_column_color_picker');
-    if (!columnTitle.val()) {
+    const $columnTitle = $('#new_project_column');
+    const $projectColorInput = $('#new_project_column_color_picker');
+    if (!$columnTitle.val()) {
       return;
     }
-    const url = $(this).data('url');
-    createNewColumn(url, columnTitle, projectColorInput);
+    const url = e.target.getAttribute('data-url');
+    createNewColumn(url, $columnTitle, $projectColorInput);
   });
 }
-
-function setLabelColor(label, color) {
-  const {r, g, b} = tinycolor(color).toRgb();
-  if (useLightTextOnBackground(r, g, b)) {
-    label.removeClass('dark-label').addClass('light-label');
-  } else {
-    label.removeClass('light-label').addClass('dark-label');
-  }
-}
-
-function rgbToHex(rgb) {
-  rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+).*\)$/);
-  return `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}`;
-}
-
-function hex(x) {
-  const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
-  return Number.isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16];
-}
diff --git a/web_src/js/features/repo-release.js b/web_src/js/features/repo-release.js
index 3338c2874b..f3cfa74418 100644
--- a/web_src/js/features/repo-release.js
+++ b/web_src/js/features/repo-release.js
@@ -1,19 +1,19 @@
-import $ from 'jquery';
 import {hideElem, showElem} from '../utils/dom.js';
 import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
 
 export function initRepoRelease() {
-  $(document).on('click', '.remove-rel-attach', function() {
-    const uuid = $(this).data('uuid');
-    const id = $(this).data('id');
-    $(`input[name='attachment-del-${uuid}']`).attr('value', true);
-    hideElem($(`#attachment-${id}`));
+  document.addEventListener('click', (e) => {
+    if (e.target.matches('.remove-rel-attach')) {
+      const uuid = e.target.getAttribute('data-uuid');
+      const id = e.target.getAttribute('data-id');
+      document.querySelector(`input[name='attachment-del-${uuid}']`).value = 'true';
+      hideElem(`#attachment-${id}`);
+    }
   });
 }
 
 export function initRepoReleaseNew() {
-  const $repoReleaseNew = $('.repository.new.release');
-  if (!$repoReleaseNew.length) return;
+  if (!document.querySelector('.repository.new.release')) return;
 
   initTagNameEditor();
   initRepoReleaseEditor();
@@ -30,8 +30,9 @@ function initTagNameEditor() {
   const newTagHelperText = el.getAttribute('data-tag-helper-new');
   const existingTagHelperText = el.getAttribute('data-tag-helper-existing');
 
-  document.getElementById('tag-name').addEventListener('keyup', (e) => {
-    const value = e.target.value;
+  const tagNameInput = document.getElementById('tag-name');
+  const hideTargetInput = function(tagNameInput) {
+    const value = tagNameInput.value;
     const tagHelper = document.getElementById('tag-helper');
     if (existingTags.includes(value)) {
       // If the tag already exists, hide the target branch selector.
@@ -41,13 +42,17 @@ function initTagNameEditor() {
       showElem('#tag-target-selector');
       tagHelper.textContent = value ? newTagHelperText : defaultTagHelperText;
     }
+  };
+  hideTargetInput(tagNameInput); // update on page load because the input may have a value
+  tagNameInput.addEventListener('input', (e) => {
+    hideTargetInput(e.target);
   });
 }
 
 function initRepoReleaseEditor() {
-  const $editor = $('.repository.new.release .combo-markdown-editor');
-  if ($editor.length === 0) {
+  const editor = document.querySelector('.repository.new.release .combo-markdown-editor');
+  if (!editor) {
     return;
   }
-  const _promise = initComboMarkdownEditor($editor);
+  initComboMarkdownEditor(editor);
 }
diff --git a/web_src/js/features/repo-search.js b/web_src/js/features/repo-search.js
new file mode 100644
index 0000000000..185f6119d9
--- /dev/null
+++ b/web_src/js/features/repo-search.js
@@ -0,0 +1,22 @@
+export function initRepositorySearch() {
+  const repositorySearchForm = document.querySelector('#repo-search-form');
+  if (!repositorySearchForm) return;
+
+  repositorySearchForm.addEventListener('change', (e) => {
+    e.preventDefault();
+
+    const formData = new FormData(repositorySearchForm);
+    const params = new URLSearchParams(formData);
+
+    if (e.target.name === 'clear-filter') {
+      params.delete('archived');
+      params.delete('fork');
+      params.delete('mirror');
+      params.delete('template');
+      params.delete('private');
+    }
+
+    params.delete('clear-filter');
+    window.location.search = params.toString();
+  });
+}
diff --git a/web_src/js/features/repo-settings.js b/web_src/js/features/repo-settings.js
index 75e624a6a7..52c5de2bfa 100644
--- a/web_src/js/features/repo-settings.js
+++ b/web_src/js/features/repo-settings.js
@@ -2,27 +2,29 @@ import $ from 'jquery';
 import {minimatch} from 'minimatch';
 import {createMonaco} from './codeeditor.js';
 import {onInputDebounce, toggleElem} from '../utils/dom.js';
+import {POST} from '../modules/fetch.js';
 
 const {appSubUrl, csrfToken} = window.config;
 
 export function initRepoSettingsCollaboration() {
   // Change collaborator access mode
-  $('.page-content.repository .ui.dropdown.access-mode').each((_, e) => {
-    const $dropdown = $(e);
+  $('.page-content.repository .ui.dropdown.access-mode').each((_, el) => {
+    const $dropdown = $(el);
     const $text = $dropdown.find('> .text');
     $dropdown.dropdown({
-      action(_text, value) {
-        const lastValue = $dropdown.attr('data-last-value');
-        $.post($dropdown.attr('data-url'), {
-          _csrf: csrfToken,
-          uid: $dropdown.attr('data-uid'),
-          mode: value,
-        }).fail(() => {
+      async action(_text, value) {
+        const lastValue = el.getAttribute('data-last-value');
+        try {
+          el.setAttribute('data-last-value', value);
+          $dropdown.dropdown('hide');
+          const data = new FormData();
+          data.append('uid', el.getAttribute('data-uid'));
+          data.append('mode', value);
+          await POST(el.getAttribute('data-url'), {data});
+        } catch {
           $text.text('(error)'); // prevent from misleading users when error occurs
-          $dropdown.attr('data-last-value', lastValue);
-        });
-        $dropdown.attr('data-last-value', value);
-        $dropdown.dropdown('hide');
+          el.setAttribute('data-last-value', lastValue);
+        }
       },
       onChange(_value, text, _$choice) {
         $text.text(text); // update the text when using keyboard navigating
@@ -30,61 +32,69 @@ export function initRepoSettingsCollaboration() {
       onHide() {
         // set to the really selected value, defer to next tick to make sure `action` has finished its work because the calling order might be onHide -> action
         setTimeout(() => {
-          const $item = $dropdown.dropdown('get item', $dropdown.attr('data-last-value'));
+          const $item = $dropdown.dropdown('get item', el.getAttribute('data-last-value'));
           if ($item) {
-            $dropdown.dropdown('set selected', $dropdown.attr('data-last-value'));
+            $dropdown.dropdown('set selected', el.getAttribute('data-last-value'));
           } else {
             $text.text('(none)'); // prevent from misleading users when the access mode is undefined
           }
         }, 0);
-      }
+      },
     });
   });
 }
 
 export function initRepoSettingSearchTeamBox() {
-  const $searchTeamBox = $('#search-team-box');
-  $searchTeamBox.search({
+  const searchTeamBox = document.getElementById('search-team-box');
+  if (!searchTeamBox) return;
+
+  $(searchTeamBox).search({
     minCharacters: 2,
     apiSettings: {
-      url: `${appSubUrl}/org/${$searchTeamBox.attr('data-org-name')}/teams/-/search?q={query}`,
+      url: `${appSubUrl}/org/${searchTeamBox.getAttribute('data-org-name')}/teams/-/search?q={query}`,
       headers: {'X-Csrf-Token': csrfToken},
       onResponse(response) {
         const items = [];
         $.each(response.data, (_i, item) => {
           items.push({
             title: item.name,
-            description: `${item.permission} access` // TODO: translate this string
+            description: `${item.permission} access`, // TODO: translate this string
           });
         });
 
         return {results: items};
-      }
+      },
     },
     searchFields: ['name', 'description'],
-    showNoResults: false
+    showNoResults: false,
   });
 }
 
 export function initRepoSettingGitHook() {
-  if ($('.edit.githook').length === 0) return;
+  if (!$('.edit.githook').length) return;
   const filename = document.querySelector('.hook-filename').textContent;
   const _promise = createMonaco($('#content')[0], filename, {language: 'shell'});
 }
 
 export function initRepoSettingBranches() {
-  if (!$('.repository.settings.branches').length) return;
-  $('.toggle-target-enabled').on('change', function () {
-    const $target = $($(this).attr('data-target'));
-    $target.toggleClass('disabled', !this.checked);
-  });
-  $('.toggle-target-disabled').on('change', function () {
-    const $target = $($(this).attr('data-target'));
-    if (this.checked) $target.addClass('disabled'); // only disable, do not auto enable
-  });
-  $('#dismiss_stale_approvals').on('change', function () {
-    const $target = $('#ignore_stale_approvals_box');
-    $target.toggleClass('disabled', this.checked);
+  if (!document.querySelector('.repository.settings.branches')) return;
+
+  for (const el of document.getElementsByClassName('toggle-target-enabled')) {
+    el.addEventListener('change', function () {
+      const target = document.querySelector(this.getAttribute('data-target'));
+      target?.classList.toggle('disabled', !this.checked);
+    });
+  }
+
+  for (const el of document.getElementsByClassName('toggle-target-disabled')) {
+    el.addEventListener('change', function () {
+      const target = document.querySelector(this.getAttribute('data-target'));
+      if (this.checked) target?.classList.add('disabled'); // only disable, do not auto enable
+    });
+  }
+
+  document.getElementById('dismiss_stale_approvals')?.addEventListener('change', function () {
+    document.getElementById('ignore_stale_approvals_box')?.classList.toggle('disabled', this.checked);
   });
 
   // show the `Matched` mark for the status checks that match the pattern
@@ -102,7 +112,6 @@ export function initRepoSettingBranches() {
           break;
         }
       }
-
       toggleElem(el, matched);
     }
   };
diff --git a/web_src/js/features/repo-template.js b/web_src/js/features/repo-template.js
index 1e83e74780..5f63e8b3ba 100644
--- a/web_src/js/features/repo-template.js
+++ b/web_src/js/features/repo-template.js
@@ -29,13 +29,13 @@ export function initRepoTemplateSearch() {
             const filteredResponse = {success: true, results: []};
             filteredResponse.results.push({
               name: '',
-              value: ''
+              value: '',
             });
             // Parse the response from the api to work with our dropdown
             $.each(response.data, (_r, repo) => {
               filteredResponse.results.push({
                 name: htmlEscape(repo.repository.full_name),
-                value: repo.repository.id
+                value: repo.repository.id,
               });
             });
             return filteredResponse;
@@ -43,7 +43,7 @@ export function initRepoTemplateSearch() {
           cache: false,
         },
 
-        fullTextSearch: true
+        fullTextSearch: true,
       });
   };
   $('#uid').on('change', changeOwner);
diff --git a/web_src/js/features/repo-unicode-escape.js b/web_src/js/features/repo-unicode-escape.js
index 6a201ec4d1..d878532001 100644
--- a/web_src/js/features/repo-unicode-escape.js
+++ b/web_src/js/features/repo-unicode-escape.js
@@ -1,31 +1,27 @@
-import $ from 'jquery';
-import {hideElem, showElem} from '../utils/dom.js';
+import {hideElem, queryElemSiblings, showElem, toggleElem} from '../utils/dom.js';
 
 export function initUnicodeEscapeButton() {
-  $(document).on('click', '.escape-button', (e) => {
+  document.addEventListener('click', (e) => {
+    const btn = e.target.closest('.escape-button, .unescape-button, .toggle-escape-button');
+    if (!btn) return;
+
     e.preventDefault();
-    $(e.target).parents('.file-content, .non-diff-file-content').find('.file-code, .file-view').addClass('unicode-escaped');
-    hideElem($(e.target));
-    showElem($(e.target).siblings('.unescape-button'));
-  });
-  $(document).on('click', '.unescape-button', (e) => {
-    e.preventDefault();
-    $(e.target).parents('.file-content, .non-diff-file-content').find('.file-code, .file-view').removeClass('unicode-escaped');
-    hideElem($(e.target));
-    showElem($(e.target).siblings('.escape-button'));
-  });
-  $(document).on('click', '.toggle-escape-button', (e) => {
-    e.preventDefault();
-    const fileContent = $(e.target).parents('.file-content, .non-diff-file-content');
-    const fileView = fileContent.find('.file-code, .file-view');
-    if (fileView.hasClass('unicode-escaped')) {
-      fileView.removeClass('unicode-escaped');
-      hideElem(fileContent.find('.unescape-button'));
-      showElem(fileContent.find('.escape-button'));
-    } else {
-      fileView.addClass('unicode-escaped');
-      showElem(fileContent.find('.unescape-button'));
-      hideElem(fileContent.find('.escape-button'));
+
+    const fileContent = btn.closest('.file-content, .non-diff-file-content');
+    const fileView = fileContent?.querySelectorAll('.file-code, .file-view');
+    if (btn.matches('.escape-button')) {
+      for (const el of fileView) el.classList.add('unicode-escaped');
+      hideElem(btn);
+      showElem(queryElemSiblings(btn, '.unescape-button'));
+    } else if (btn.matches('.unescape-button')) {
+      for (const el of fileView) el.classList.remove('unicode-escaped');
+      hideElem(btn);
+      showElem(queryElemSiblings(btn, '.escape-button'));
+    } else if (btn.matches('.toggle-escape-button')) {
+      const isEscaped = fileView[0]?.classList.contains('unicode-escaped');
+      for (const el of fileView) el.classList.toggle('unicode-escaped', !isEscaped);
+      toggleElem(fileContent.querySelectorAll('.unescape-button'), !isEscaped);
+      toggleElem(fileContent.querySelectorAll('.escape-button'), isEscaped);
     }
   });
 }
diff --git a/web_src/js/features/repo-wiki.js b/web_src/js/features/repo-wiki.js
index 58036fde37..03a2c68c5a 100644
--- a/web_src/js/features/repo-wiki.js
+++ b/web_src/js/features/repo-wiki.js
@@ -1,50 +1,51 @@
-import $ from 'jquery';
 import {initMarkupContent} from '../markup/content.js';
 import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
 import {fomanticMobileScreen} from '../modules/fomantic.js';
-
-const {csrfToken} = window.config;
+import {POST} from '../modules/fetch.js';
 
 async function initRepoWikiFormEditor() {
-  const $editArea = $('.repository.wiki .combo-markdown-editor textarea');
-  if (!$editArea.length) return;
+  const editArea = document.querySelector('.repository.wiki .combo-markdown-editor textarea');
+  if (!editArea) return;
 
-  const $form = $('.repository.wiki.new .ui.form');
-  const $editorContainer = $form.find('.combo-markdown-editor');
+  const form = document.querySelector('.repository.wiki.new .ui.form');
+  const editorContainer = form.querySelector('.combo-markdown-editor');
   let editor;
 
   let renderRequesting = false;
   let lastContent;
-  const renderEasyMDEPreview = function () {
+  const renderEasyMDEPreview = async function () {
     if (renderRequesting) return;
 
-    const $previewFull = $editorContainer.find('.EasyMDEContainer .editor-preview-active');
-    const $previewSide = $editorContainer.find('.EasyMDEContainer .editor-preview-active-side');
-    const $previewTarget = $previewSide.length ? $previewSide : $previewFull;
-    const newContent = $editArea.val();
-    if (editor && $previewTarget.length && lastContent !== newContent) {
+    const previewFull = editorContainer.querySelector('.EasyMDEContainer .editor-preview-active');
+    const previewSide = editorContainer.querySelector('.EasyMDEContainer .editor-preview-active-side');
+    const previewTarget = previewSide || previewFull;
+    const newContent = editArea.value;
+    if (editor && previewTarget && lastContent !== newContent) {
       renderRequesting = true;
-      $.post(editor.previewUrl, {
-        _csrf: csrfToken,
-        mode: editor.previewMode,
-        context: editor.previewContext,
-        text: newContent,
-        wiki: editor.previewWiki,
-      }).done((data) => {
+      const formData = new FormData();
+      formData.append('mode', editor.previewMode);
+      formData.append('context', editor.previewContext);
+      formData.append('text', newContent);
+      formData.append('wiki', editor.previewWiki);
+      try {
+        const response = await POST(editor.previewUrl, {data: formData});
+        const data = await response.text();
         lastContent = newContent;
-        $previewTarget.html(`<div class="markup ui segment">${data}</div>`);
+        previewTarget.innerHTML = `<div class="markup ui segment">${data}</div>`;
         initMarkupContent();
-      }).always(() => {
+      } catch (error) {
+        console.error('Error rendering preview:', error);
+      } finally {
         renderRequesting = false;
         setTimeout(renderEasyMDEPreview, 1000);
-      });
+      }
     } else {
       setTimeout(renderEasyMDEPreview, 1000);
     }
   };
   renderEasyMDEPreview();
 
-  editor = await initComboMarkdownEditor($editorContainer, {
+  editor = await initComboMarkdownEditor(editorContainer, {
     useScene: 'wiki',
     // EasyMDE has some problems of height definition, it has inline style height 300px by default, so we also use inline styles to override it.
     // And another benefit is that we only need to write the style once for both editors.
@@ -59,14 +60,15 @@ async function initRepoWikiFormEditor() {
         'gitea-code-inline', 'code', 'quote', '|', 'gitea-checkbox-empty', 'gitea-checkbox-checked', '|',
         'unordered-list', 'ordered-list', '|',
         'link', 'image', 'table', 'horizontal-rule', '|',
-        'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea'
+        'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea',
       ],
     },
   });
 
-  $form.on('submit', () => {
-    if (!validateTextareaNonEmpty($editArea)) {
-      return false;
+  form.addEventListener('submit', (e) => {
+    if (!validateTextareaNonEmpty(editArea)) {
+      e.preventDefault();
+      e.stopPropagation();
     }
   });
 }
diff --git a/web_src/js/features/sshkey-helper.js b/web_src/js/features/sshkey-helper.js
index 099b54d3a6..3960eefe8e 100644
--- a/web_src/js/features/sshkey-helper.js
+++ b/web_src/js/features/sshkey-helper.js
@@ -1,12 +1,10 @@
-import $ from 'jquery';
-
 export function initSshKeyFormParser() {
-// Parse SSH Key
-  $('#ssh-key-content').on('change paste keyup', function () {
-    const arrays = $(this).val().split(' ');
-    const $title = $('#ssh-key-title');
-    if ($title.val() === '' && arrays.length === 3 && arrays[2] !== '') {
-      $title.val(arrays[2]);
+  // Parse SSH Key
+  document.getElementById('ssh-key-content')?.addEventListener('input', function () {
+    const arrays = this.value.split(' ');
+    const title = document.getElementById('ssh-key-title');
+    if (!title.value && arrays.length === 3 && arrays[2] !== '') {
+      title.value = arrays[2];
     }
   });
 }
diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js
index f43014fec5..2ec74344fc 100644
--- a/web_src/js/features/stopwatch.js
+++ b/web_src/js/features/stopwatch.js
@@ -1,8 +1,9 @@
-import $ from 'jquery';
 import prettyMilliseconds from 'pretty-ms';
 import {createTippy} from '../modules/tippy.js';
+import {GET} from '../modules/fetch.js';
+import {hideElem, showElem} from '../utils/dom.js';
 
-const {appSubUrl, csrfToken, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config;
+const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config;
 
 export function initStopwatch() {
   if (!enableTimeTracking) {
@@ -28,7 +29,7 @@ export function initStopwatch() {
   });
 
   // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used.
-  const currSeconds = $('.stopwatch-time').attr('data-seconds');
+  const currSeconds = document.querySelector('.stopwatch-time')?.getAttribute('data-seconds');
   if (currSeconds) {
     updateStopwatchTime(currSeconds);
   }
@@ -74,7 +75,7 @@ export function initStopwatch() {
           type: 'close',
         });
         worker.port.close();
-        window.location.href = appSubUrl;
+        window.location.href = `${appSubUrl}/`;
       } else if (event.data.type === 'close') {
         worker.port.postMessage({
           type: 'close',
@@ -112,29 +113,31 @@ async function updateStopwatchWithCallback(callback, timeout) {
 }
 
 async function updateStopwatch() {
-  const data = await $.ajax({
-    type: 'GET',
-    url: `${appSubUrl}/user/stopwatches`,
-    headers: {'X-Csrf-Token': csrfToken},
-  });
+  const response = await GET(`${appSubUrl}/user/stopwatches`);
+  if (!response.ok) {
+    console.error('Failed to fetch stopwatch data');
+    return false;
+  }
+  const data = await response.json();
   return updateStopwatchData(data);
 }
 
 function updateStopwatchData(data) {
   const watch = data[0];
-  const btnEl = $('.active-stopwatch-trigger');
+  const btnEl = document.querySelector('.active-stopwatch-trigger');
   if (!watch) {
     clearStopwatchTimer();
-    btnEl.addClass('gt-hidden');
+    hideElem(btnEl);
   } else {
     const {repo_owner_name, repo_name, issue_index, seconds} = watch;
     const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
-    $('.stopwatch-link').attr('href', issueUrl);
-    $('.stopwatch-commit').attr('action', `${issueUrl}/times/stopwatch/toggle`);
-    $('.stopwatch-cancel').attr('action', `${issueUrl}/times/stopwatch/cancel`);
-    $('.stopwatch-issue').text(`${repo_owner_name}/${repo_name}#${issue_index}`);
+    document.querySelector('.stopwatch-link')?.setAttribute('href', issueUrl);
+    document.querySelector('.stopwatch-commit')?.setAttribute('action', `${issueUrl}/times/stopwatch/toggle`);
+    document.querySelector('.stopwatch-cancel')?.setAttribute('action', `${issueUrl}/times/stopwatch/cancel`);
+    const stopwatchIssue = document.querySelector('.stopwatch-issue');
+    if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`;
     updateStopwatchTime(seconds);
-    btnEl.removeClass('gt-hidden');
+    showElem(btnEl);
   }
   return Boolean(data.length);
 }
@@ -151,12 +154,13 @@ function updateStopwatchTime(seconds) {
   if (!Number.isFinite(secs)) return;
 
   clearStopwatchTimer();
-  const $stopwatch = $('.stopwatch-time');
+  const stopwatch = document.querySelector('.stopwatch-time');
+  // TODO: replace with <relative-time> similar to how system status up time is shown
   const start = Date.now();
   const updateUi = () => {
     const delta = Date.now() - start;
     const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
-    $stopwatch.text(dur);
+    if (stopwatch) stopwatch.textContent = dur;
   };
   updateUi();
   updateTimeIntervalId = setInterval(updateUi, 1000);
diff --git a/web_src/js/features/tribute.js b/web_src/js/features/tribute.js
index 055777be79..02cd484374 100644
--- a/web_src/js/features/tribute.js
+++ b/web_src/js/features/tribute.js
@@ -25,7 +25,7 @@ function makeCollections({mentions, emoji}) {
       },
       menuItemTemplate: (item) => {
         return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`;
-      }
+      },
     });
   }
 
@@ -36,12 +36,12 @@ function makeCollections({mentions, emoji}) {
       menuItemTemplate: (item) => {
         return `
           <div class="tribute-item">
-            <img src="${htmlEscape(item.original.avatar)}" class="gt-mr-3"/>
+            <img src="${htmlEscape(item.original.avatar)}" class="tw-mr-2"/>
             <span class="name">${htmlEscape(item.original.name)}</span>
             ${item.original.fullname && item.original.fullname !== '' ? `<span class="fullname">${htmlEscape(item.original.fullname)}</span>` : ''}
           </div>
         `;
-      }
+      },
     });
   }
 
diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js
index 363e039760..6dfbb4d765 100644
--- a/web_src/js/features/user-auth-webauthn.js
+++ b/web_src/js/features/user-auth-webauthn.js
@@ -26,7 +26,7 @@ export async function initUserAuthWebAuthn() {
   }
   try {
     const credential = await navigator.credentials.get({
-      publicKey: options.publicKey
+      publicKey: options.publicKey,
     });
     await verifyAssertion(credential);
   } catch (err) {
@@ -37,7 +37,7 @@ export async function initUserAuthWebAuthn() {
     delete options.publicKey.extensions.appid;
     try {
       const credential = await navigator.credentials.get({
-        publicKey: options.publicKey
+        publicKey: options.publicKey,
       });
       await verifyAssertion(credential);
     } catch (err) {
@@ -185,7 +185,7 @@ async function webAuthnRegisterRequest() {
 
   try {
     const credential = await navigator.credentials.create({
-      publicKey: options.publicKey
+      publicKey: options.publicKey,
     });
     await webauthnRegistered(credential);
   } catch (err) {
diff --git a/web_src/js/features/user-auth.js b/web_src/js/features/user-auth.js
index af380dcfc7..a871ac471c 100644
--- a/web_src/js/features/user-auth.js
+++ b/web_src/js/features/user-auth.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
 import {checkAppUrl} from './common-global.js';
 
 export function initUserAuthOauth2() {
@@ -10,41 +9,14 @@ export function initUserAuthOauth2() {
 
   for (const link of outer.querySelectorAll('.oauth-login-link')) {
     link.addEventListener('click', () => {
-      inner.classList.add('gt-invisible');
+      inner.classList.add('tw-invisible');
       outer.classList.add('is-loading');
       setTimeout(() => {
         // recover previous content to let user try again
         // usually redirection will be performed before this action
         outer.classList.remove('is-loading');
-        inner.classList.remove('gt-invisible');
+        inner.classList.remove('tw-invisible');
       }, 5000);
     });
   }
 }
-
-export function initUserAuthLinkAccountView() {
-  const $lnkUserPage = $('.page-content.user.link-account');
-  if ($lnkUserPage.length === 0) {
-    return false;
-  }
-
-  const $signinTab = $lnkUserPage.find('.item[data-tab="auth-link-signin-tab"]');
-  const $signUpTab = $lnkUserPage.find('.item[data-tab="auth-link-signup-tab"]');
-  const $signInView = $lnkUserPage.find('.tab[data-tab="auth-link-signin-tab"]');
-  const $signUpView = $lnkUserPage.find('.tab[data-tab="auth-link-signup-tab"]');
-
-  $signUpTab.on('click', () => {
-    $signinTab.removeClass('active');
-    $signInView.removeClass('active');
-    $signUpTab.addClass('active');
-    $signUpView.addClass('active');
-    return false;
-  });
-
-  $signinTab.on('click', () => {
-    $signUpTab.removeClass('active');
-    $signUpView.removeClass('active');
-    $signinTab.addClass('active');
-    $signInView.addClass('active');
-  });
-}
diff --git a/web_src/js/features/user-settings.js b/web_src/js/features/user-settings.js
index d49bf39275..2d8c53e457 100644
--- a/web_src/js/features/user-settings.js
+++ b/web_src/js/features/user-settings.js
@@ -1,18 +1,19 @@
-import $ from 'jquery';
 import {hideElem, showElem} from '../utils/dom.js';
 
 export function initUserSettings() {
-  if ($('.user.settings.profile').length > 0) {
-    $('#username').on('keyup', function () {
-      const $prompt = $('#name-change-prompt');
-      const $prompt_redirect = $('#name-change-redirect-prompt');
-      if ($(this).val().toString().toLowerCase() !== $(this).data('name').toString().toLowerCase()) {
-        showElem($prompt);
-        showElem($prompt_redirect);
-      } else {
-        hideElem($prompt);
-        hideElem($prompt_redirect);
-      }
-    });
-  }
+  if (!document.querySelectorAll('.user.settings.profile').length) return;
+
+  const usernameInput = document.getElementById('username');
+  if (!usernameInput) return;
+  usernameInput.addEventListener('input', function () {
+    const prompt = document.getElementById('name-change-prompt');
+    const promptRedirect = document.getElementById('name-change-redirect-prompt');
+    if (this.value.toLowerCase() !== this.getAttribute('data-name').toLowerCase()) {
+      showElem(prompt);
+      showElem(promptRedirect);
+    } else {
+      hideElem(prompt);
+      hideElem(promptRedirect);
+    }
+  });
 }
diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js
index 92400d1cbe..5ca3018308 100644
--- a/web_src/js/htmx.js
+++ b/web_src/js/htmx.js
@@ -1,6 +1,9 @@
 import * as htmx from 'htmx.org';
 import {showErrorToast} from './modules/toast.js';
 
+// https://github.com/bigskysoftware/idiomorph#htmx
+import 'idiomorph/dist/idiomorph-ext.js';
+
 // https://htmx.org/reference/#config
 htmx.config.requestClass = 'is-loading';
 htmx.config.scrollIntoViewOnBoost = false;
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 4713618506..fc2f6b9b0b 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -13,6 +13,7 @@ import {initImageDiff} from './features/imagediff.js';
 import {initRepoMigration} from './features/repo-migration.js';
 import {initRepoProject} from './features/repo-projects.js';
 import {initTableSort} from './features/tablesort.js';
+import {initAutoFocusEnd} from './features/autofocus-end.js';
 import {initAdminUserListSearchForm} from './features/admin/users.js';
 import {initAdminConfigs} from './features/admin/config.js';
 import {initMarkupAnchors} from './markup/anchors.js';
@@ -23,7 +24,7 @@ import {initFindFileInRepo} from './features/repo-findfile.js';
 import {initCommentContent, initMarkupContent} from './markup/content.js';
 import {initPdfViewer} from './render/pdf.js';
 
-import {initUserAuthLinkAccountView, initUserAuthOauth2} from './features/user-auth.js';
+import {initUserAuthOauth2} from './features/user-auth.js';
 import {
   initRepoIssueDue,
   initRepoIssueReferenceRepositorySearch,
@@ -33,11 +34,7 @@ import {
   initRepoPullRequestAllowMaintainerEdit,
   initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
 } from './features/repo-issue.js';
-import {
-  initRepoEllipsisButton,
-  initRepoCommitLastCommitLoader,
-  initCommitStatuses,
-} from './features/repo-commit.js';
+import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.js';
 import {
   initFootLanguageMenu,
   initGlobalButtonClickOnEnter,
@@ -83,8 +80,13 @@ import {initGiteaFomantic} from './modules/fomantic.js';
 import {onDomReady} from './utils/dom.js';
 import {initRepoIssueList} from './features/repo-issue-list.js';
 import {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
+import {initRepoContributors} from './features/contributors.js';
+import {initRepoCodeFrequency} from './features/code-frequency.js';
+import {initRepoRecentCommits} from './features/recent-commits.js';
 import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
 import {initDirAuto} from './modules/dirauto.js';
+import {initRepositorySearch} from './features/repo-search.js';
+import {initColorPickers} from './features/colorpicker.js';
 
 // Init Gitea's Fomantic settings
 initGiteaFomantic();
@@ -122,6 +124,7 @@ onDomReady(() => {
   initSshKeyFormParser();
   initStopwatch();
   initTableSort();
+  initAutoFocusEnd();
   initFindFileInRepo();
   initCopyContent();
 
@@ -145,7 +148,6 @@ onDomReady(() => {
   initRepoCommentForm();
   initRepoEllipsisButton();
   initRepoDiffCommitBranchesAndTags();
-  initRepoCommitLastCommitLoader();
   initRepoEditor();
   initRepoGraphGit();
   initRepoIssueContentHistory();
@@ -172,11 +174,14 @@ onDomReady(() => {
   initRepoWikiForm();
   initRepository();
   initRepositoryActionView();
+  initRepositorySearch();
+  initRepoContributors();
+  initRepoCodeFrequency();
+  initRepoRecentCommits();
 
   initCommitStatuses();
   initCaptcha();
 
-  initUserAuthLinkAccountView();
   initUserAuthOauth2();
   initUserAuthWebAuthn();
   initUserAuthWebAuthnRegister();
@@ -184,4 +189,5 @@ onDomReady(() => {
   initRepoDiffView();
   initPdfViewer();
   initScopedAccessTokenCategories();
+  initColorPickers();
 });
diff --git a/web_src/js/jquery.js b/web_src/js/jquery.js
index 892e2763cb..6b2199896c 100644
--- a/web_src/js/jquery.js
+++ b/web_src/js/jquery.js
@@ -1,3 +1,3 @@
 import $ from 'jquery';
 
-window.$ = window.jQuery = $;
+window.$ = window.jQuery = $; // eslint-disable-line no-jquery/variable-pattern
diff --git a/web_src/js/markup/anchors.js b/web_src/js/markup/anchors.js
index 53dfa2980c..0e2c92713a 100644
--- a/web_src/js/markup/anchors.js
+++ b/web_src/js/markup/anchors.js
@@ -1,35 +1,70 @@
 import {svg} from '../svg.js';
 
-const headingSelector = '.markup h1, .markup h2, .markup h3, .markup h4, .markup h5, .markup h6';
+const addPrefix = (str) => `user-content-${str}`;
+const removePrefix = (str) => str.replace(/^user-content-/, '');
+const hasPrefix = (str) => str.startsWith('user-content-');
 
-function scrollToAnchor(hash, initial) {
-  // abort if the browser has already scrolled to another anchor during page load
-  if (initial && document.querySelector(':target')) return;
-  if (hash?.length <= 1) return;
-  const id = decodeURIComponent(hash.substring(1));
-  const el = document.getElementById(`user-content-${id}`);
-  if (el) {
-    el.scrollIntoView();
-  } else if (id.startsWith('user-content-')) { // compat for links with old 'user-content-' prefixed hashes
-    const el = document.getElementById(id);
-    if (el) el.scrollIntoView();
+// scroll to anchor while respecting the `user-content` prefix that exists on the target
+function scrollToAnchor(encodedId) {
+  if (!encodedId) return;
+  const id = decodeURIComponent(encodedId);
+  const prefixedId = addPrefix(id);
+  let el = document.getElementById(prefixedId);
+
+  // check for matching user-generated `a[name]`
+  if (!el) {
+    const nameAnchors = document.getElementsByName(prefixedId);
+    if (nameAnchors.length) {
+      el = nameAnchors[0];
+    }
   }
+
+  // compat for links with old 'user-content-' prefixed hashes
+  if (!el && hasPrefix(id)) {
+    return document.getElementById(id)?.scrollIntoView();
+  }
+
+  el?.scrollIntoView();
 }
 
 export function initMarkupAnchors() {
-  if (!document.querySelector('.markup')) return;
+  const markupEls = document.querySelectorAll('.markup');
+  if (!markupEls.length) return;
 
-  for (const heading of document.querySelectorAll(headingSelector)) {
-    const originalId = heading.id.replace(/^user-content-/, '');
-    const a = document.createElement('a');
-    a.classList.add('anchor');
-    a.setAttribute('href', `#${encodeURIComponent(originalId)}`);
-    a.innerHTML = svg('octicon-link');
-    a.addEventListener('click', (e) => {
-      scrollToAnchor(e.currentTarget.getAttribute('href'), false);
-    });
-    heading.prepend(a);
+  for (const markupEl of markupEls) {
+    // create link icons for markup headings, the resulting link href will remove `user-content-`
+    for (const heading of markupEl.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
+      const a = document.createElement('a');
+      a.classList.add('anchor');
+      a.setAttribute('href', `#${encodeURIComponent(removePrefix(heading.id))}`);
+      a.innerHTML = svg('octicon-link');
+      heading.prepend(a);
+    }
+
+    // remove `user-content-` prefix from links so they don't show in url bar when clicked
+    for (const a of markupEl.querySelectorAll('a[href^="#"]')) {
+      const href = a.getAttribute('href');
+      if (!href.startsWith('#user-content-')) continue;
+      a.setAttribute('href', `#${removePrefix(href.substring(1))}`);
+    }
+
+    // add `user-content-` prefix to user-generated `a[name]` link targets
+    // TODO: this prefix should be added in backend instead
+    for (const a of markupEl.querySelectorAll('a[name]')) {
+      const name = a.getAttribute('name');
+      if (!name) continue;
+      a.setAttribute('name', addPrefix(a.name));
+    }
+
+    for (const a of markupEl.querySelectorAll('a[href^="#"]')) {
+      a.addEventListener('click', (e) => {
+        scrollToAnchor(e.currentTarget.getAttribute('href')?.substring(1));
+      });
+    }
   }
 
-  scrollToAnchor(window.location.hash, true);
+  // scroll to anchor unless the browser has already scrolled somewhere during page load
+  if (!document.querySelector(':target')) {
+    scrollToAnchor(window.location.hash?.substring(1));
+  }
 }
diff --git a/web_src/js/markup/mermaid.js b/web_src/js/markup/mermaid.js
index 84d88a94c3..0549fb3e31 100644
--- a/web_src/js/markup/mermaid.js
+++ b/web_src/js/markup/mermaid.js
@@ -45,11 +45,11 @@ export async function renderMermaid() {
       const {svg} = await mermaid.render('mermaid', source);
 
       const iframe = document.createElement('iframe');
-      iframe.classList.add('markup-render', 'gt-invisible');
+      iframe.classList.add('markup-render', 'tw-invisible');
       iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`;
 
       const mermaidBlock = document.createElement('div');
-      mermaidBlock.classList.add('mermaid-block', 'is-loading', 'gt-hidden');
+      mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden');
       mermaidBlock.append(iframe);
 
       const btn = makeCodeCopyButton();
@@ -58,11 +58,11 @@ export async function renderMermaid() {
 
       iframe.addEventListener('load', () => {
         pre.replaceWith(mermaidBlock);
-        mermaidBlock.classList.remove('gt-hidden');
+        mermaidBlock.classList.remove('tw-hidden');
         iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`;
         setTimeout(() => { // avoid flash of iframe background
           mermaidBlock.classList.remove('is-loading');
-          iframe.classList.remove('gt-invisible');
+          iframe.classList.remove('tw-invisible');
         }, 0);
       });
 
diff --git a/web_src/js/markup/tasklist.js b/web_src/js/markup/tasklist.js
index ad1c6964a7..00076bce58 100644
--- a/web_src/js/markup/tasklist.js
+++ b/web_src/js/markup/tasklist.js
@@ -1,4 +1,4 @@
-import $ from 'jquery';
+import {POST} from '../modules/fetch.js';
 
 const preventListener = (e) => e.preventDefault();
 
@@ -55,12 +55,11 @@ export function initMarkupTasklist() {
           const updateUrl = editContentZone.getAttribute('data-update-url');
           const context = editContentZone.getAttribute('data-context');
 
-          await $.post(updateUrl, {
-            ignore_attachments: true,
-            _csrf: window.config.csrfToken,
-            content: newContent,
-            context
-          });
+          const requestBody = new FormData();
+          requestBody.append('ignore_attachments', 'true');
+          requestBody.append('content', newContent);
+          requestBody.append('context', context);
+          await POST(updateUrl, {data: requestBody});
 
           rawContent.textContent = newContent;
         } catch (err) {
diff --git a/web_src/js/modules/fetch.js b/web_src/js/modules/fetch.js
index b3529d27fc..2191a8d4db 100644
--- a/web_src/js/modules/fetch.js
+++ b/web_src/js/modules/fetch.js
@@ -8,19 +8,17 @@ const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
 // fetch wrapper, use below method name functions and the `data` option to pass in data
 // which will automatically set an appropriate headers. For json content, only object
 // and array types are currently supported.
-export function request(url, {method = 'GET', headers = {}, data, body, ...other} = {}) {
-  let contentType;
-  if (!body) {
-    if (data instanceof FormData || data instanceof URLSearchParams) {
-      body = data;
-    } else if (isObject(data) || Array.isArray(data)) {
-      contentType = 'application/json';
-      body = JSON.stringify(data);
-    }
+export function request(url, {method = 'GET', data, headers = {}, ...other} = {}) {
+  let body, contentType;
+  if (data instanceof FormData || data instanceof URLSearchParams) {
+    body = data;
+  } else if (isObject(data) || Array.isArray(data)) {
+    contentType = 'application/json';
+    body = JSON.stringify(data);
   }
 
   const headersMerged = new Headers({
-    ...(!safeMethods.has(method.toUpperCase()) && {'x-csrf-token': csrfToken}),
+    ...(!safeMethods.has(method) && {'x-csrf-token': csrfToken}),
     ...(contentType && {'content-type': contentType}),
   });
 
@@ -31,8 +29,8 @@ export function request(url, {method = 'GET', headers = {}, data, body, ...other
   return fetch(url, {
     method,
     headers: headersMerged,
-    ...(body && {body}),
     ...other,
+    ...(body && {body}),
   });
 }
 
diff --git a/web_src/js/modules/fomantic.js b/web_src/js/modules/fomantic.js
index 0c7a7ae641..d205c2b2ee 100644
--- a/web_src/js/modules/fomantic.js
+++ b/web_src/js/modules/fomantic.js
@@ -11,13 +11,11 @@ export const fomanticMobileScreen = window.matchMedia('only screen and (max-widt
 export function initGiteaFomantic() {
   // Silence fomantic's error logging when tabs are used without a target content element
   $.fn.tab.settings.silent = true;
-  // Disable the behavior of fomantic to toggle the checkbox when you press enter on a checkbox element.
-  $.fn.checkbox.settings.enableEnterKey = false;
 
   // By default, use "exact match" for full text search
   $.fn.dropdown.settings.fullTextSearch = 'exact';
   // Do not use "cursor: pointer" for dropdown labels
-  $.fn.dropdown.settings.className.label += ' gt-cursor-default';
+  $.fn.dropdown.settings.className.label += ' tw-cursor-default';
   // Always use Gitea's SVG icons
   $.fn.dropdown.settings.templates.label = function(_value, text, preserveHTML, className) {
     const escape = $.fn.dropdown.settings.templates.escape;
diff --git a/web_src/js/modules/fomantic/aria.md b/web_src/js/modules/fomantic/aria.md
index a32d15f46f..5836a34506 100644
--- a/web_src/js/modules/fomantic/aria.md
+++ b/web_src/js/modules/fomantic/aria.md
@@ -2,10 +2,10 @@
 
 This document is used as aria/accessibility(a11y) reference for future developers.
 
-There are a lot of a11y problems in the Fomantic UI library. This `aria.js` is used
-as a workaround to make the UI more accessible.
+There are a lot of a11y problems in the Fomantic UI library. Files in 
+`web_src/js/modules/fomantic/` are used as a workaround to make the UI more accessible.
 
-The `aria.js` is designed to avoid touching the official Fomantic UI library,
+The aria-related code is designed to avoid touching the official Fomantic UI library,
 and to be as independent as possible, so it can be easily modified/removed in the future.
 
 To test the aria/accessibility with screen readers, developers can use the following steps:
@@ -14,7 +14,7 @@ To test the aria/accessibility with screen readers, developers can use the follo
   * Press `Command + F5` to turn on VoiceOver.
   * Try to operate the UI with keyboard-only.
   * Use Tab/Shift+Tab to switch focus between elements.
-  * Arrow keys to navigate between menu/combobox items (only aria-active, not really focused).
+  * Arrow keys (Option+Up/Down) to navigate between menu/combobox items (only aria-active, not really focused).
   * Press Enter to trigger the aria-active element.
 * On Android, you can use TalkBack.
   * Go to Settings -> Accessibility -> TalkBack, turn it on.
@@ -41,24 +41,19 @@ The ideal checkboxes should be:
 <label><input type="checkbox"> ... </label>
 ```
 
-However, related CSS styles aren't supported (not implemented) yet, so at the moment,
-almost all the checkboxes are still using Fomantic UI checkbox.
-
-## Fomantic UI Checkbox
+However, the templates still have the Fomantic-style HTML layout:
 
 ```html
 <div class="ui checkbox">
-  <input type="checkbox"> <!-- class "hidden" will be added by $.checkbox() -->
+  <input type="checkbox">
   <label>...</label>
 </div>
 ```
 
-Then the JS `$.checkbox()` should be called to make it work with keyboard and label-clicking,
-then it works like the ideal checkboxes.
-
-There is still a problem: Fomantic UI checkbox is not friendly to screen readers,
-so we add IDs to all the Fomantic UI checkboxes automatically by JS.
-If the `label` part is empty, then the checkbox needs to get the `aria-label` attribute manually.
+We call `initAriaCheckboxPatch` to link the `input` and `label` which makes clicking the
+label etc. work. There is still a problem: These checkboxes are not friendly to screen readers,
+so we add IDs to all the Fomantic UI checkboxes automatically by JS. If the `label` part is empty,
+then the checkbox needs to get the `aria-label` attribute manually.
 
 # Fomantic Dropdown
 
@@ -75,7 +70,7 @@ Fomantic Dropdown is designed to be used for many purposes:
 Fomantic Dropdown requires that the focus must be on its primary element.
 If the focus changes, it hides or panics.
 
-At the moment, `aria.js` only tries to partially resolve the a11y problems for dropdowns with items.
+At the moment, the aria-related code only tries to partially resolve the a11y problems for dropdowns with items.
 
 There are different solutions:
 
diff --git a/web_src/js/modules/fomantic/checkbox.js b/web_src/js/modules/fomantic/checkbox.js
index 08af1c2eb6..7f2b340296 100644
--- a/web_src/js/modules/fomantic/checkbox.js
+++ b/web_src/js/modules/fomantic/checkbox.js
@@ -1,38 +1,24 @@
-import $ from 'jquery';
 import {generateAriaId} from './base.js';
 
-const ariaPatchKey = '_giteaAriaPatchCheckbox';
-const fomanticCheckboxFn = $.fn.checkbox;
-
-// use our own `$.fn.checkbox` to patch Fomantic's checkbox module
 export function initAriaCheckboxPatch() {
-  if ($.fn.checkbox === ariaCheckboxFn) throw new Error('initAriaCheckboxPatch could only be called once');
-  $.fn.checkbox = ariaCheckboxFn;
-  ariaCheckboxFn.settings = fomanticCheckboxFn.settings;
-}
+  // link the label and the input element so it's clickable and accessible
+  for (const el of document.querySelectorAll('.ui.checkbox')) {
+    if (el.hasAttribute('data-checkbox-patched')) continue;
+    const label = el.querySelector('label');
+    const input = el.querySelector('input');
+    if (!label || !input) continue;
+    const inputId = input.getAttribute('id');
+    const labelFor = label.getAttribute('for');
 
-// the patched `$.fn.checkbox` checkbox function
-// * it does the one-time attaching on the first call
-function ariaCheckboxFn(...args) {
-  const ret = fomanticCheckboxFn.apply(this, args);
-  for (const el of this) {
-    if (el[ariaPatchKey]) continue;
-    attachInit(el);
+    if (inputId && !labelFor) { // missing "for"
+      label.setAttribute('for', inputId);
+    } else if (!inputId && !labelFor) { // missing both "id" and "for"
+      const id = generateAriaId();
+      input.setAttribute('id', id);
+      label.setAttribute('for', id);
+    } else {
+      continue;
+    }
+    el.setAttribute('data-checkbox-patched', 'true');
   }
-  return ret;
-}
-
-function attachInit(el) {
-  // Fomantic UI checkbox needs to be something like: <div class="ui checkbox"><label /><input /></div>
-  // It doesn't work well with <label><input />...</label>
-  // To make it work with aria, the "id"/"for" attributes are necessary, so add them automatically if missing.
-  // In the future, refactor to use native checkbox directly, then this patch could be removed.
-  el[ariaPatchKey] = {}; // record that this element has been patched
-  const label = el.querySelector('label');
-  const input = el.querySelector('input');
-  if (!label || !input || input.getAttribute('id')) return;
-
-  const id = generateAriaId();
-  input.setAttribute('id', id);
-  label.setAttribute('for', id);
 }
diff --git a/web_src/js/modules/fomantic/dropdown.js b/web_src/js/modules/fomantic/dropdown.js
index c053256dd5..82e710860d 100644
--- a/web_src/js/modules/fomantic/dropdown.js
+++ b/web_src/js/modules/fomantic/dropdown.js
@@ -21,12 +21,11 @@ function ariaDropdownFn(...args) {
   // it means that this call will reset the dropdown internal settings, then we need to re-delegate the callbacks.
   const needDelegate = (!args.length || typeof args[0] !== 'string');
   for (const el of this) {
-    const $dropdown = $(el);
     if (!el[ariaPatchKey]) {
-      attachInit($dropdown);
+      attachInit(el);
     }
     if (needDelegate) {
-      delegateOne($dropdown);
+      delegateOne($(el));
     }
   }
   return ret;
@@ -38,19 +37,25 @@ function updateMenuItem(dropdown, item) {
   if (!item.id) item.id = generateAriaId();
   item.setAttribute('role', dropdown[ariaPatchKey].listItemRole);
   item.setAttribute('tabindex', '-1');
-  for (const a of item.querySelectorAll('a')) a.setAttribute('tabindex', '-1');
+  for (const el of item.querySelectorAll('a, input, button')) el.setAttribute('tabindex', '-1');
 }
-
-// make the label item and its "delete icon" has correct aria attributes
-function updateSelectionLabel($label) {
+/**
+ * make the label item and its "delete icon" have correct aria attributes
+ * @param {HTMLElement} label
+ */
+function updateSelectionLabel(label) {
   // the "label" is like this: "<a|div class="ui label" data-value="1">the-label-name <i|svg class="delete icon"/></a>"
-  if (!$label.attr('id')) $label.attr('id', generateAriaId());
-  $label.attr('tabindex', '-1');
-  $label.find('.delete.icon').attr({
-    'aria-hidden': 'false',
-    'aria-label': window.config.i18n.remove_label_str.replace('%s', $label.attr('data-value')),
-    'role': 'button',
-  });
+  if (!label.id) {
+    label.id = generateAriaId();
+  }
+  label.tabIndex = -1;
+
+  const deleteIcon = label.querySelector('.delete.icon');
+  if (deleteIcon) {
+    deleteIcon.setAttribute('aria-hidden', 'false');
+    deleteIcon.setAttribute('aria-label', window.config.i18n.remove_label_str.replace('%s', label.getAttribute('data-value')));
+    deleteIcon.setAttribute('role', 'button');
+  }
 }
 
 // delegate the dropdown's template functions and callback functions to add aria attributes.
@@ -72,7 +77,9 @@ function delegateOne($dropdown) {
   dropdownTemplates.menu = function(response, fields, preserveHTML, className) {
     // when the dropdown menu items are loaded from AJAX requests, the items are created dynamically
     const menuItems = dropdownTemplatesMenuOld(response, fields, preserveHTML, className);
-    const $wrapper = $('<div>').append(menuItems);
+    const div = document.createElement('div');
+    div.innerHTML = menuItems;
+    const $wrapper = $(div);
     const $items = $wrapper.find('> .item');
     $items.each((_, item) => updateMenuItem($dropdown[0], item));
     $dropdown[0][ariaPatchKey].deferredRefreshAriaActiveItem();
@@ -84,43 +91,44 @@ function delegateOne($dropdown) {
   const dropdownOnLabelCreateOld = dropdownCall('setting', 'onLabelCreate');
   dropdownCall('setting', 'onLabelCreate', function(value, text) {
     const $label = dropdownOnLabelCreateOld.call(this, value, text);
-    updateSelectionLabel($label);
+    updateSelectionLabel($label[0]);
     return $label;
   });
 }
 
 // for static dropdown elements (generated by server-side template), prepare them with necessary aria attributes
-function attachStaticElements($dropdown, $focusable, $menu) {
-  const dropdown = $dropdown[0];
-
+function attachStaticElements(dropdown, focusable, menu) {
   // prepare static dropdown menu list popup
-  if (!$menu.attr('id')) $menu.attr('id', generateAriaId());
-  $menu.find('> .item').each((_, item) => updateMenuItem(dropdown, item));
+  if (!menu.id) {
+    menu.id = generateAriaId();
+  }
+
+  $(menu).find('> .item').each((_, item) => updateMenuItem(dropdown, item));
+
   // this role could only be changed after its content is ready, otherwise some browsers+readers (like Chrome+AppleVoice) crash
-  $menu.attr('role', dropdown[ariaPatchKey].listPopupRole);
+  menu.setAttribute('role', dropdown[ariaPatchKey].listPopupRole);
 
   // prepare selection label items
-  $dropdown.find('.ui.label').each((_, label) => updateSelectionLabel($(label)));
+  for (const label of dropdown.querySelectorAll('.ui.label')) {
+    updateSelectionLabel(label);
+  }
 
   // make the primary element (focusable) aria-friendly
-  $focusable.attr({
-    'role': $focusable.attr('role') ?? dropdown[ariaPatchKey].focusableRole,
-    'aria-haspopup': dropdown[ariaPatchKey].listPopupRole,
-    'aria-controls': $menu.attr('id'),
-    'aria-expanded': 'false',
-  });
+  focusable.setAttribute('role', focusable.getAttribute('role') ?? dropdown[ariaPatchKey].focusableRole);
+  focusable.setAttribute('aria-haspopup', dropdown[ariaPatchKey].listPopupRole);
+  focusable.setAttribute('aria-controls', menu.id);
+  focusable.setAttribute('aria-expanded', 'false');
 
   // use tooltip's content as aria-label if there is no aria-label
-  const tooltipContent = $dropdown.attr('data-tooltip-content');
-  if (tooltipContent && !$dropdown.attr('aria-label')) {
-    $dropdown.attr('aria-label', tooltipContent);
+  const tooltipContent = dropdown.getAttribute('data-tooltip-content');
+  if (tooltipContent && !dropdown.getAttribute('aria-label')) {
+    dropdown.setAttribute('aria-label', tooltipContent);
   }
 }
 
-function attachInit($dropdown) {
-  const dropdown = $dropdown[0];
+function attachInit(dropdown) {
   dropdown[ariaPatchKey] = {};
-  if ($dropdown.hasClass('custom')) return;
+  if (dropdown.classList.contains('custom')) return;
 
   // Dropdown has 2 different focusing behaviors
   // * with search input: the input is focused, and it works with aria-activedescendant pointing another sibling element.
@@ -137,67 +145,69 @@ function attachInit($dropdown) {
 
   // TODO: multiple selection is only partially supported. Check and test them one by one in the future.
 
-  const $textSearch = $dropdown.find('input.search').eq(0);
-  const $focusable = $textSearch.length ? $textSearch : $dropdown; // the primary element for focus, see comment above
-  if (!$focusable.length) return;
+  const textSearch = dropdown.querySelector('input.search');
+  const focusable = textSearch || dropdown; // the primary element for focus, see comment above
+  if (!focusable) return;
 
   // as a combobox, the input should not have autocomplete by default
-  if ($textSearch.length && !$textSearch.attr('autocomplete')) {
-    $textSearch.attr('autocomplete', 'off');
+  if (textSearch && !textSearch.getAttribute('autocomplete')) {
+    textSearch.setAttribute('autocomplete', 'off');
   }
 
-  let $menu = $dropdown.find('> .menu');
-  if (!$menu.length) {
+  let menu = $(dropdown).find('> .menu')[0];
+  if (!menu) {
     // some "multiple selection" dropdowns don't have a static menu element in HTML, we need to pre-create it to make it have correct aria attributes
-    $menu = $('<div class="menu"></div>').appendTo($dropdown);
+    menu = document.createElement('div');
+    menu.classList.add('menu');
+    dropdown.append(menu);
   }
 
   // There are 2 possible solutions about the role: combobox or menu.
   // The idea is that if there is an input, then it's a combobox, otherwise it's a menu.
   // Since #19861 we have prepared the "combobox" solution, but didn't get enough time to put it into practice and test before.
-  const isComboBox = $dropdown.find('input').length > 0;
+  const isComboBox = dropdown.querySelectorAll('input').length > 0;
 
   dropdown[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'menu';
   dropdown[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : '';
   dropdown[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem';
 
-  attachDomEvents($dropdown, $focusable, $menu);
-  attachStaticElements($dropdown, $focusable, $menu);
+  attachDomEvents(dropdown, focusable, menu);
+  attachStaticElements(dropdown, focusable, menu);
 }
 
-function attachDomEvents($dropdown, $focusable, $menu) {
-  const dropdown = $dropdown[0];
+function attachDomEvents(dropdown, focusable, menu) {
   // when showing, it has class: ".animating.in"
   // when hiding, it has class: ".visible.animating.out"
-  const isMenuVisible = () => ($menu.hasClass('visible') && !$menu.hasClass('out')) || $menu.hasClass('in');
+  const isMenuVisible = () => (menu.classList.contains('visible') && !menu.classList.contains('out')) || menu.classList.contains('in');
 
   // update aria attributes according to current active/selected item
   const refreshAriaActiveItem = () => {
     const menuVisible = isMenuVisible();
-    $focusable.attr('aria-expanded', menuVisible ? 'true' : 'false');
+    focusable.setAttribute('aria-expanded', menuVisible ? 'true' : 'false');
 
     // if there is an active item, use it (the user is navigating between items)
     // otherwise use the "selected" for combobox (for the last selected item)
-    const $active = $menu.find('> .item.active, > .item.selected');
+    const active = $(menu).find('> .item.active, > .item.selected')[0];
+    if (!active) return;
     // if the popup is visible and has an active/selected item, use its id as aria-activedescendant
     if (menuVisible) {
-      $focusable.attr('aria-activedescendant', $active.attr('id'));
+      focusable.setAttribute('aria-activedescendant', active.id);
     } else if (dropdown[ariaPatchKey].listPopupRole === 'menu') {
       // for menu, when the popup is hidden, no need to keep the aria-activedescendant, and clear the active/selected item
-      $focusable.removeAttr('aria-activedescendant');
-      $active.removeClass('active').removeClass('selected');
+      focusable.removeAttribute('aria-activedescendant');
+      active.classList.remove('active', 'selected');
     }
   };
 
-  $dropdown.on('keydown', (e) => {
+  dropdown.addEventListener('keydown', (e) => {
     // here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler
     if (e.key === 'Enter') {
-      const dropdownCall = fomanticDropdownFn.bind($dropdown);
+      const dropdownCall = fomanticDropdownFn.bind($(dropdown));
       let $item = dropdownCall('get item', dropdownCall('get value'));
-      if (!$item) $item = $menu.find('> .item.selected'); // when dropdown filters items by input, there is no "value", so query the "selected" item
+      if (!$item) $item = $(menu).find('> .item.selected'); // when dropdown filters items by input, there is no "value", so query the "selected" item
       // if the selected item is clickable, then trigger the click event.
       // we can not click any item without check, because Fomantic code might also handle the Enter event. that would result in double click.
-      if ($item && ($item.is('a') || $item.hasClass('js-aria-clickable'))) $item[0].click();
+      if ($item?.[0]?.matches('a, .js-aria-clickable')) $item[0].click();
     }
   });
 
@@ -207,7 +217,7 @@ function attachDomEvents($dropdown, $focusable, $menu) {
   // without the delay for hiding, the UI will be somewhat laggy and sometimes may get stuck in the animation.
   const deferredRefreshAriaActiveItem = (delay = 0) => { setTimeout(refreshAriaActiveItem, delay) };
   dropdown[ariaPatchKey].deferredRefreshAriaActiveItem = deferredRefreshAriaActiveItem;
-  $dropdown.on('keyup', (e) => { if (e.key.startsWith('Arrow')) deferredRefreshAriaActiveItem(); });
+  dropdown.addEventListener('keyup', (e) => { if (e.key.startsWith('Arrow')) deferredRefreshAriaActiveItem(); });
 
   // if the dropdown has been opened by focus, do not trigger the next click event again.
   // otherwise the dropdown will be closed immediately, especially on Android with TalkBack
diff --git a/web_src/js/modules/fomantic/modal.js b/web_src/js/modules/fomantic/modal.js
index 7c9aade790..8b455cf4de 100644
--- a/web_src/js/modules/fomantic/modal.js
+++ b/web_src/js/modules/fomantic/modal.js
@@ -19,7 +19,9 @@ function ariaModalFn(...args) {
       // In such case, the "Enter" key will trigger the "cancel" button instead of "ok" button, then the dialog will be closed.
       // It breaks the user experience - the "Enter" key should confirm the dialog and submit the form.
       // So, all "cancel" buttons without "[type]" must be marked as "type=button".
-      $(el).find('form button.cancel:not([type])').attr('type', 'button');
+      for (const button of el.querySelectorAll('form button.cancel:not([type])')) {
+        button.setAttribute('type', 'button');
+      }
     }
   }
   return ret;
diff --git a/web_src/js/modules/sortable.js b/web_src/js/modules/sortable.js
index cfe7c3bf30..1c9adb6d72 100644
--- a/web_src/js/modules/sortable.js
+++ b/web_src/js/modules/sortable.js
@@ -1,4 +1,19 @@
-export async function createSortable(...args) {
+export async function createSortable(el, opts = {}) {
   const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs');
-  return new Sortable(...args);
+
+  return new Sortable(el, {
+    animation: 150,
+    ghostClass: 'card-ghost',
+    onChoose: (e) => {
+      const handle = opts.handle ? e.item.querySelector(opts.handle) : e.item;
+      handle.classList.add('tw-cursor-grabbing');
+      opts.onChoose?.(e);
+    },
+    onUnchoose: (e) => {
+      const handle = opts.handle ? e.item.querySelector(opts.handle) : e.item;
+      handle.classList.remove('tw-cursor-grabbing');
+      opts.onUnchoose?.(e);
+    },
+    ...opts,
+  });
 }
diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js
index 27f371fd88..83b28e5745 100644
--- a/web_src/js/modules/tippy.js
+++ b/web_src/js/modules/tippy.js
@@ -1,12 +1,15 @@
 import tippy, {followCursor} from 'tippy.js';
 import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
+import {formatDatetime} from '../utils/time.js';
 
 const visibleInstances = new Set();
+const arrowSvg = `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`;
 
 export function createTippy(target, opts = {}) {
   // the callback functions should be destructured from opts,
   // because we should use our own wrapper functions to handle them, do not let the user override them
-  const {onHide, onShow, onDestroy, ...other} = opts;
+  const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
+
   const instance = tippy(target, {
     appendTo: document.body,
     animation: false,
@@ -33,18 +36,15 @@ export function createTippy(target, opts = {}) {
       visibleInstances.add(instance);
       return onShow?.(instance);
     },
-    arrow: `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`,
-    role: 'menu', // HTML role attribute, only tooltips should use "tooltip"
-    theme: other.role || 'menu', // CSS theme, either "tooltip", "menu" or "box-with-header"
+    arrow: arrow || (theme === 'bare' ? false : arrowSvg),
+    role: role || 'menu', // HTML role attribute
+    theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare"
     plugins: [followCursor],
     ...other,
   });
 
-  // for popups where content refers to a DOM element, we use the 'tippy-target' class
-  // to initially hide the content, now we can remove it as the content has been removed
-  // from the DOM by tippy
-  if (other.content instanceof Element) {
-    other.content.classList.remove('tippy-target');
+  if (role === 'menu') {
+    target.setAttribute('aria-haspopup', 'true');
   }
 
   return instance;
@@ -93,8 +93,15 @@ function attachTooltip(target, content = null) {
 }
 
 function switchTitleToTooltip(target) {
-  const title = target.getAttribute('title');
+  let title = target.getAttribute('title');
   if (title) {
+    // apply custom formatting to relative-time's tooltips
+    if (target.tagName.toLowerCase() === 'relative-time') {
+      const datetime = target.getAttribute('datetime');
+      if (datetime) {
+        title = formatDatetime(new Date(datetime));
+      }
+    }
     target.setAttribute('data-tooltip-content', title);
     target.setAttribute('aria-label', title);
     // keep the attribute, in case there are some other "[title]" selectors
@@ -142,7 +149,7 @@ export function initGlobalTooltips() {
   const observerConnect = (observer) => observer.observe(document, {
     subtree: true,
     childList: true,
-    attributeFilter: ['data-tooltip-content', 'title']
+    attributeFilter: ['data-tooltip-content', 'title'],
   });
   const observer = new MutationObserver((mutationList, observer) => {
     const pending = observer.takeRecords();
diff --git a/web_src/js/modules/toast.js b/web_src/js/modules/toast.js
index fa075aed48..d12d203718 100644
--- a/web_src/js/modules/toast.js
+++ b/web_src/js/modules/toast.js
@@ -21,13 +21,12 @@ const levels = {
 };
 
 // See https://github.com/apvarun/toastify-js#api for options
-function showToast(message, level, {gravity, position, duration, ...other} = {}) {
+function showToast(message, level, {gravity, position, duration, useHtmlBody, ...other} = {}) {
   const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
-
   const toast = Toastify({
     text: `
       <div class='toast-icon'>${svg(icon)}</div>
-      <div class='toast-body'>${htmlEscape(message)}</div>
+      <div class='toast-body'>${useHtmlBody ? message : htmlEscape(message)}</div>
       <button class='toast-close'>${svg('octicon-x')}</button>
     `,
     escapeMarkup: false,
@@ -40,6 +39,7 @@ function showToast(message, level, {gravity, position, duration, ...other} = {})
 
   toast.showToast();
   toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast());
+  return toast;
 }
 
 export function showInfoToast(message, opts) {
diff --git a/web_src/js/standalone/swagger.js b/web_src/js/standalone/swagger.js
index cb91089daf..00854ef5d7 100644
--- a/web_src/js/standalone/swagger.js
+++ b/web_src/js/standalone/swagger.js
@@ -21,11 +21,11 @@ window.addEventListener('load', async () => {
     docExpansion: 'none',
     defaultModelRendering: 'model', // don't show examples by default, because they may be incomplete
     presets: [
-      SwaggerUI.presets.apis
+      SwaggerUI.presets.apis,
     ],
     plugins: [
-      SwaggerUI.plugins.DownloadUrl
-    ]
+      SwaggerUI.plugins.DownloadUrl,
+    ],
   });
 
   window.ui = ui;
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 084256587c..913d26779f 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -67,6 +67,7 @@ import octiconStrikethrough from '../../public/assets/img/svg/octicon-strikethro
 import octiconSync from '../../public/assets/img/svg/octicon-sync.svg';
 import octiconTable from '../../public/assets/img/svg/octicon-table.svg';
 import octiconTag from '../../public/assets/img/svg/octicon-tag.svg';
+import octiconTrash from '../../public/assets/img/svg/octicon-trash.svg';
 import octiconTriangleDown from '../../public/assets/img/svg/octicon-triangle-down.svg';
 import octiconX from '../../public/assets/img/svg/octicon-x.svg';
 import octiconXCircleFill from '../../public/assets/img/svg/octicon-x-circle-fill.svg';
@@ -139,6 +140,7 @@ const svgs = {
   'octicon-sync': octiconSync,
   'octicon-table': octiconTable,
   'octicon-tag': octiconTag,
+  'octicon-trash': octiconTrash,
   'octicon-triangle-down': octiconTriangleDown,
   'octicon-x': octiconX,
   'octicon-x-circle-fill': octiconXCircleFill,
@@ -187,7 +189,7 @@ export const SvgIcon = {
     name: {type: String, required: true},
     size: {type: Number, default: 16},
     className: {type: String, default: ''},
-    symbolId: {type: String}
+    symbolId: {type: String},
   },
   render() {
     let {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name);
@@ -211,7 +213,7 @@ export const SvgIcon = {
       classes.push(...this.className.split(/\s+/).filter(Boolean));
     }
     if (this.symbolId) {
-      classes.push('gt-hidden', 'svg-symbol-container');
+      classes.push('tw-hidden', 'svg-symbol-container');
       svgInnerHtml = `<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${svgInnerHtml}</symbol>`;
     }
     // create VNode
diff --git a/web_src/js/utils.js b/web_src/js/utils.js
index c82e42d349..ce0fb66343 100644
--- a/web_src/js/utils.js
+++ b/web_src/js/utils.js
@@ -2,13 +2,14 @@ import {encode, decode} from 'uint8-to-base64';
 
 // transform /path/to/file.ext to file.ext
 export function basename(path = '') {
-  return path ? path.replace(/^.*\//, '') : '';
+  const lastSlashIndex = path.lastIndexOf('/');
+  return lastSlashIndex < 0 ? path : path.substring(lastSlashIndex + 1);
 }
 
 // transform /path/to/file.ext to .ext
 export function extname(path = '') {
-  const [_, ext] = /.+(\.[^.]+)$/.exec(path) || [];
-  return ext || '';
+  const lastPointIndex = path.lastIndexOf('.');
+  return lastPointIndex < 0 ? '' : path.substring(lastPointIndex);
 }
 
 // test whether a variable is an object
@@ -139,3 +140,5 @@ export function parseDom(text, contentType) {
 export function serializeXml(node) {
   return xmlSerializer.serializeToString(node);
 }
+
+export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
diff --git a/web_src/js/utils/color.js b/web_src/js/utils/color.js
index 5d9c4ca45d..198f97c454 100644
--- a/web_src/js/utils/color.js
+++ b/web_src/js/utils/color.js
@@ -1,21 +1,33 @@
-// Check similar implementation in modules/util/color.go and keep synchronization
-// Return R, G, B values defined in reletive luminance
-function getLuminanceRGB(channel) {
-  const sRGB = channel / 255;
-  return (sRGB <= 0.03928) ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ** 2.4;
+import tinycolor from 'tinycolor2';
+
+// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
+// Keep this in sync with modules/util/color.go
+function getRelativeLuminance(color) {
+  const {r, g, b} = tinycolor(color).toRgb();
+  return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
 }
 
-// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
-function getLuminance(r, g, b) {
-  const R = getLuminanceRGB(r);
-  const G = getLuminanceRGB(g);
-  const B = getLuminanceRGB(b);
-  return 0.2126 * R + 0.7152 * G + 0.0722 * B;
+function useLightText(backgroundColor) {
+  return getRelativeLuminance(backgroundColor) < 0.453;
 }
 
-// Reference from: https://firsching.ch/github_labels.html
-// In the future WCAG 3 APCA may be a better solution.
-// Check if text should use light color based on RGB of background
-export function useLightTextOnBackground(r, g, b) {
-  return getLuminance(r, g, b) < 0.453;
+// Given a background color, returns a black or white foreground color that the highest
+// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
+// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
+export function contrastColor(backgroundColor) {
+  return useLightText(backgroundColor) ? '#fff' : '#000';
 }
+
+function resolveColors(obj) {
+  const styles = window.getComputedStyle(document.documentElement);
+  const getColor = (name) => styles.getPropertyValue(name).trim();
+  return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
+}
+
+export const chartJsColors = resolveColors({
+  text: '--color-text',
+  border: '--color-secondary-alpha-60',
+  commits: '--color-primary-alpha-60',
+  additions: '--color-green',
+  deletions: '--color-red',
+});
diff --git a/web_src/js/utils/color.test.js b/web_src/js/utils/color.test.js
index e129109ef0..fee9afc776 100644
--- a/web_src/js/utils/color.test.js
+++ b/web_src/js/utils/color.test.js
@@ -1,21 +1,22 @@
-import {useLightTextOnBackground} from './color.js';
+import {contrastColor} from './color.js';
 
-test('useLightTextOnBackground', () => {
-  expect(useLightTextOnBackground(215, 58, 74)).toBe(true);
-  expect(useLightTextOnBackground(0, 117, 202)).toBe(true);
-  expect(useLightTextOnBackground(207, 211, 215)).toBe(false);
-  expect(useLightTextOnBackground(162, 238, 239)).toBe(false);
-  expect(useLightTextOnBackground(112, 87, 255)).toBe(true);
-  expect(useLightTextOnBackground(0, 134, 114)).toBe(true);
-  expect(useLightTextOnBackground(228, 230, 105)).toBe(false);
-  expect(useLightTextOnBackground(216, 118, 227)).toBe(true);
-  expect(useLightTextOnBackground(255, 255, 255)).toBe(false);
-  expect(useLightTextOnBackground(43, 134, 133)).toBe(true);
-  expect(useLightTextOnBackground(43, 135, 134)).toBe(true);
-  expect(useLightTextOnBackground(44, 135, 134)).toBe(true);
-  expect(useLightTextOnBackground(59, 182, 179)).toBe(true);
-  expect(useLightTextOnBackground(124, 114, 104)).toBe(true);
-  expect(useLightTextOnBackground(126, 113, 108)).toBe(true);
-  expect(useLightTextOnBackground(129, 112, 109)).toBe(true);
-  expect(useLightTextOnBackground(128, 112, 112)).toBe(true);
+test('contrastColor', () => {
+  expect(contrastColor('#d73a4a')).toBe('#fff');
+  expect(contrastColor('#0075ca')).toBe('#fff');
+  expect(contrastColor('#cfd3d7')).toBe('#000');
+  expect(contrastColor('#a2eeef')).toBe('#000');
+  expect(contrastColor('#7057ff')).toBe('#fff');
+  expect(contrastColor('#008672')).toBe('#fff');
+  expect(contrastColor('#e4e669')).toBe('#000');
+  expect(contrastColor('#d876e3')).toBe('#000');
+  expect(contrastColor('#ffffff')).toBe('#000');
+  expect(contrastColor('#2b8684')).toBe('#fff');
+  expect(contrastColor('#2b8786')).toBe('#fff');
+  expect(contrastColor('#2c8786')).toBe('#000');
+  expect(contrastColor('#3bb6b3')).toBe('#000');
+  expect(contrastColor('#7c7268')).toBe('#fff');
+  expect(contrastColor('#7e716c')).toBe('#fff');
+  expect(contrastColor('#81706d')).toBe('#fff');
+  expect(contrastColor('#807070')).toBe('#fff');
+  expect(contrastColor('#84b6eb')).toBe('#000');
 });
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
index 4dc55a518a..fb23a71725 100644
--- a/web_src/js/utils/dom.js
+++ b/web_src/js/utils/dom.js
@@ -22,11 +22,11 @@ function elementsCall(el, func, ...args) {
  */
 function toggleShown(el, force) {
   if (force === true) {
-    el.classList.remove('gt-hidden');
+    el.classList.remove('tw-hidden');
   } else if (force === false) {
-    el.classList.add('gt-hidden');
+    el.classList.add('tw-hidden');
   } else if (force === undefined) {
-    el.classList.toggle('gt-hidden');
+    el.classList.toggle('tw-hidden');
   } else {
     throw new Error('invalid force argument');
   }
@@ -46,11 +46,29 @@ export function toggleElem(el, force) {
 
 export function isElemHidden(el) {
   const res = [];
-  elementsCall(el, (e) => res.push(e.classList.contains('gt-hidden')));
+  elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden')));
   if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`);
   return res[0];
 }
 
+function applyElemsCallback(elems, fn) {
+  if (fn) {
+    for (const el of elems) {
+      fn(el);
+    }
+  }
+  return elems;
+}
+
+export function queryElemSiblings(el, selector = '*', fn) {
+  return applyElemsCallback(Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector)), fn);
+}
+
+// it works like jQuery.children: only the direct children are selected
+export function queryElemChildren(parent, selector = '*', fn) {
+  return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn);
+}
+
 export function onDomReady(cb) {
   if (document.readyState === 'loading') {
     document.addEventListener('DOMContentLoaded', cb);
@@ -187,7 +205,7 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
       textarea.removeEventListener('mousemove', onUserResize);
       textarea.removeEventListener('input', resizeToFit);
       textarea.form?.removeEventListener('reset', onFormReset);
-    }
+    },
   };
 }
 
@@ -211,6 +229,7 @@ export function loadElem(el, src) {
 const needSubmitEventPolyfill = typeof SubmitEvent === 'undefined';
 
 export function submitEventSubmitter(e) {
+  e = e.originalEvent ?? e; // if the event is wrapped by jQuery, use "originalEvent", otherwise, use the event itself
   return needSubmitEventPolyfill ? (e.target._submitter || null) : e.submitter;
 }
 
@@ -226,3 +245,51 @@ export function initSubmitEventPolyfill() {
   document.body.addEventListener('click', submitEventPolyfillListener);
   document.body.addEventListener('focus', submitEventPolyfillListener);
 }
+
+/**
+ * Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
+ * Note: This function doesn't account for all possible visibility scenarios.
+ * @param {HTMLElement} element The element to check.
+ * @returns {boolean} True if the element is visible.
+ */
+export function isElemVisible(element) {
+  if (!element) return false;
+
+  return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
+}
+
+// extract text and images from "paste" event
+export function getPastedContent(e) {
+  const images = [];
+  for (const item of e.clipboardData?.items ?? []) {
+    if (item.type?.startsWith('image/')) {
+      images.push(item.getAsFile());
+    }
+  }
+  const text = e.clipboardData?.getData?.('text') ?? '';
+  return {text, images};
+}
+
+// replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this
+export function replaceTextareaSelection(textarea, text) {
+  const before = textarea.value.slice(0, textarea.selectionStart ?? undefined);
+  const after = textarea.value.slice(textarea.selectionEnd ?? undefined);
+  let success = true;
+
+  textarea.contentEditable = 'true';
+  try {
+    success = document.execCommand('insertText', false, text);
+  } catch {
+    success = false;
+  }
+  textarea.contentEditable = 'false';
+
+  if (success && !textarea.value.slice(0, textarea.selectionStart ?? undefined).endsWith(text)) {
+    success = false;
+  }
+
+  if (!success) {
+    textarea.value = `${before}${text}${after}`;
+    textarea.dispatchEvent(new CustomEvent('change', {bubbles: true, cancelable: true}));
+  }
+}
diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js
new file mode 100644
index 0000000000..ed5d98e35a
--- /dev/null
+++ b/web_src/js/utils/image.js
@@ -0,0 +1,47 @@
+export async function pngChunks(blob) {
+  const uint8arr = new Uint8Array(await blob.arrayBuffer());
+  const chunks = [];
+  if (uint8arr.length < 12) return chunks;
+  const view = new DataView(uint8arr.buffer);
+  if (view.getBigUint64(0) !== 9894494448401390090n) return chunks;
+
+  const decoder = new TextDecoder();
+  let index = 8;
+  while (index < uint8arr.length) {
+    const len = view.getUint32(index);
+    chunks.push({
+      name: decoder.decode(uint8arr.slice(index + 4, index + 8)),
+      data: uint8arr.slice(index + 8, index + 8 + len),
+    });
+    index += len + 12;
+  }
+
+  return chunks;
+}
+
+// decode a image and try to obtain width and dppx. If will never throw but instead
+// return default values.
+export async function imageInfo(blob) {
+  let width = 0; // 0 means no width could be determined
+  let dppx = 1; // 1 dot per pixel for non-HiDPI screens
+
+  if (blob.type === 'image/png') { // only png is supported currently
+    try {
+      for (const {name, data} of await pngChunks(blob)) {
+        const view = new DataView(data.buffer);
+        if (name === 'IHDR' && data?.length) {
+          // extract width from mandatory IHDR chunk
+          width = view.getUint32(0);
+        } else if (name === 'pHYs' && data?.length) {
+          // extract dppx from optional pHYs chunk, assuming pixels are square
+          const unit = view.getUint8(8);
+          if (unit === 1) {
+            dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx
+          }
+        }
+      }
+    } catch {}
+  }
+
+  return {width, dppx};
+}
diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js
new file mode 100644
index 0000000000..ba4758250c
--- /dev/null
+++ b/web_src/js/utils/image.test.js
@@ -0,0 +1,29 @@
+import {pngChunks, imageInfo} from './image.js';
+
+const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg==';
+const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A==';
+const pngEmpty = 'data:image/png;base64,';
+
+async function dataUriToBlob(datauri) {
+  return await (await globalThis.fetch(datauri)).blob();
+}
+
+test('pngChunks', async () => {
+  expect(await pngChunks(await dataUriToBlob(pngNoPhys))).toEqual([
+    {name: 'IHDR', data: new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0])},
+    {name: 'IDAT', data: new Uint8Array([8, 29, 1, 2, 0, 253, 255, 0, 0, 0, 2, 0, 1])},
+    {name: 'IEND', data: new Uint8Array([])},
+  ]);
+  expect(await pngChunks(await dataUriToBlob(pngPhys))).toEqual([
+    {name: 'IHDR', data: new Uint8Array([0, 0, 0, 2, 0, 0, 0, 2, 8, 2, 0, 0, 0])},
+    {name: 'pHYs', data: new Uint8Array([0, 0, 22, 37, 0, 0, 22, 37, 1])},
+    {name: 'IDAT', data: new Uint8Array([8, 215, 99, 144, 53, 151, 0, 34, 6, 8, 5, 0, 11, 242, 1, 177])},
+  ]);
+  expect(await pngChunks(await dataUriToBlob(pngEmpty))).toEqual([]);
+});
+
+test('imageInfo', async () => {
+  expect(await imageInfo(await dataUriToBlob(pngNoPhys))).toEqual({width: 1, dppx: 1});
+  expect(await imageInfo(await dataUriToBlob(pngPhys))).toEqual({width: 2, dppx: 2});
+  expect(await imageInfo(await dataUriToBlob(pngEmpty))).toEqual({width: 0, dppx: 1});
+});
diff --git a/web_src/js/utils/time.js b/web_src/js/utils/time.js
new file mode 100644
index 0000000000..1848792c98
--- /dev/null
+++ b/web_src/js/utils/time.js
@@ -0,0 +1,67 @@
+import dayjs from 'dayjs';
+import {getCurrentLocale} from '../utils.js';
+
+// Returns an array of millisecond-timestamps of start-of-week days (Sundays)
+export function startDaysBetween(startDate, endDate) {
+  // Ensure the start date is a Sunday
+  while (startDate.getDay() !== 0) {
+    startDate.setDate(startDate.getDate() + 1);
+  }
+
+  const start = dayjs(startDate);
+  const end = dayjs(endDate);
+  const startDays = [];
+
+  let current = start;
+  while (current.isBefore(end)) {
+    startDays.push(current.valueOf());
+    // we are adding 7 * 24 hours instead of 1 week because we don't want
+    // date library to use local time zone to calculate 1 week from now.
+    // local time zone is problematic because of daylight saving time (dst)
+    // used on some countries
+    current = current.add(7 * 24, 'hour');
+  }
+
+  return startDays;
+}
+
+export function firstStartDateAfterDate(inputDate) {
+  if (!(inputDate instanceof Date)) {
+    throw new Error('Invalid date');
+  }
+  const dayOfWeek = inputDate.getDay();
+  const daysUntilSunday = 7 - dayOfWeek;
+  const resultDate = new Date(inputDate.getTime());
+  resultDate.setDate(resultDate.getDate() + daysUntilSunday);
+  return resultDate.valueOf();
+}
+
+export function fillEmptyStartDaysWithZeroes(startDays, data) {
+  const result = {};
+
+  for (const startDay of startDays) {
+    result[startDay] = data[startDay] || {'week': startDay, 'additions': 0, 'deletions': 0, 'commits': 0};
+  }
+
+  return Object.values(result);
+}
+
+let dateFormat;
+
+// format a Date object to document's locale, but with 24h format from user's current locale because this
+// option is a personal preference of the user, not something that the document's locale should dictate.
+export function formatDatetime(date) {
+  if (!dateFormat) {
+    // TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support
+    dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), {
+      day: 'numeric',
+      month: 'short',
+      year: 'numeric',
+      hour: 'numeric',
+      hour12: !Number.isInteger(Number(new Intl.DateTimeFormat([], {hour: 'numeric'}).format())),
+      minute: '2-digit',
+      timeZoneName: 'short',
+    });
+  }
+  return dateFormat.format(date);
+}
diff --git a/web_src/js/utils/time.test.js b/web_src/js/utils/time.test.js
new file mode 100644
index 0000000000..dd1114ce7f
--- /dev/null
+++ b/web_src/js/utils/time.test.js
@@ -0,0 +1,15 @@
+import {startDaysBetween} from './time.js';
+
+test('startDaysBetween', () => {
+  expect(startDaysBetween(new Date('2024-02-15'), new Date('2024-04-18'))).toEqual([
+    1708214400000,
+    1708819200000,
+    1709424000000,
+    1710028800000,
+    1710633600000,
+    1711238400000,
+    1711843200000,
+    1712448000000,
+    1713052800000,
+  ]);
+});
diff --git a/web_src/js/utils/url.js b/web_src/js/utils/url.js
index a40737ca6f..470ece31b0 100644
--- a/web_src/js/utils/url.js
+++ b/web_src/js/utils/url.js
@@ -1,3 +1,15 @@
 export function pathEscapeSegments(s) {
   return s.split('/').map(encodeURIComponent).join('/');
 }
+
+function stripSlash(url) {
+  return url.endsWith('/') ? url.slice(0, -1) : url;
+}
+
+export function isUrl(url) {
+  try {
+    return stripSlash((new URL(url).href)).trim() === stripSlash(url).trim();
+  } catch {
+    return false;
+  }
+}
diff --git a/web_src/js/utils/url.test.js b/web_src/js/utils/url.test.js
index 3dbedec94f..08c6373ffb 100644
--- a/web_src/js/utils/url.test.js
+++ b/web_src/js/utils/url.test.js
@@ -1,6 +1,13 @@
-import {pathEscapeSegments} from './url.js';
+import {pathEscapeSegments, isUrl} from './url.js';
 
 test('pathEscapeSegments', () => {
   expect(pathEscapeSegments('a/b/c')).toEqual('a/b/c');
   expect(pathEscapeSegments('a/b/ c')).toEqual('a/b/%20c');
 });
+
+test('isUrl', () => {
+  expect(isUrl('https://example.com')).toEqual(true);
+  expect(isUrl('https://example.com/')).toEqual(true);
+  expect(isUrl('https://example.com/index.html')).toEqual(true);
+  expect(isUrl('/index.html')).toEqual(false);
+});
diff --git a/web_src/js/test/setup.js b/web_src/js/vitest.setup.js
similarity index 100%
rename from web_src/js/test/setup.js
rename to web_src/js/vitest.setup.js
diff --git a/web_src/js/webcomponents/README.md b/web_src/js/webcomponents/README.md
index 0fde507310..45af58e1d2 100644
--- a/web_src/js/webcomponents/README.md
+++ b/web_src/js/webcomponents/README.md
@@ -6,7 +6,6 @@ https://developer.mozilla.org/en-US/docs/Web/Web_Components
 
 # Guidelines
 
-* These components are loaded in `<head>` (before DOM body),
-  so they should have their own dependencies and should be very light,
-  then they won't affect the page loading time too much.
-* If the component is not a public one, it's suggested to have its own `Gitea` or `gitea-` prefix to avoid conflicts.
+* These components are loaded in `<head>` (before DOM body) in a separate entry point, they need to be lightweight to not affect the page loading time too much.
+* Do not import `svg.js` into a web component because that file is currently not tree-shakeable, import svg files individually insteat.
+* All our components must be added to `webpack.config.js` so they work correctly in Vue.
diff --git a/web_src/js/webcomponents/absolute-date.js b/web_src/js/webcomponents/absolute-date.js
new file mode 100644
index 0000000000..d2be455302
--- /dev/null
+++ b/web_src/js/webcomponents/absolute-date.js
@@ -0,0 +1,40 @@
+import {Temporal} from 'temporal-polyfill';
+
+export function toAbsoluteLocaleDate(dateStr, lang, opts) {
+  return Temporal.PlainDate.from(dateStr).toLocaleString(lang ?? [], opts);
+}
+
+window.customElements.define('absolute-date', class extends HTMLElement {
+  static observedAttributes = ['date', 'year', 'month', 'weekday', 'day'];
+
+  update = () => {
+    const year = this.getAttribute('year') ?? '';
+    const month = this.getAttribute('month') ?? '';
+    const weekday = this.getAttribute('weekday') ?? '';
+    const day = this.getAttribute('day') ?? '';
+    const lang = this.closest('[lang]')?.getAttribute('lang') ||
+      this.ownerDocument.documentElement.getAttribute('lang') || '';
+
+    // only use the first 10 characters, e.g. the `yyyy-mm-dd` part
+    const dateStr = this.getAttribute('date').substring(0, 10);
+
+    if (!this.shadowRoot) this.attachShadow({mode: 'open'});
+    this.shadowRoot.textContent = toAbsoluteLocaleDate(dateStr, lang, {
+      ...(year && {year}),
+      ...(month && {month}),
+      ...(weekday && {weekday}),
+      ...(day && {day}),
+    });
+  };
+
+  attributeChangedCallback(_name, oldValue, newValue) {
+    if (!this.initialized || oldValue === newValue) return;
+    this.update();
+  }
+
+  connectedCallback() {
+    this.initialized = false;
+    this.update();
+    this.initialized = true;
+  }
+});
diff --git a/web_src/js/webcomponents/absolute-date.test.js b/web_src/js/webcomponents/absolute-date.test.js
new file mode 100644
index 0000000000..ba04451b65
--- /dev/null
+++ b/web_src/js/webcomponents/absolute-date.test.js
@@ -0,0 +1,15 @@
+import {toAbsoluteLocaleDate} from './absolute-date.js';
+
+test('toAbsoluteLocaleDate', () => {
+  expect(toAbsoluteLocaleDate('2024-03-15', 'en-US', {
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric',
+  })).toEqual('March 15, 2024');
+
+  expect(toAbsoluteLocaleDate('2024-03-15', 'de-DE', {
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric',
+  })).toEqual('15. März 2024');
+});
diff --git a/web_src/js/webcomponents/index.js b/web_src/js/webcomponents/index.js
new file mode 100644
index 0000000000..7cec9da734
--- /dev/null
+++ b/web_src/js/webcomponents/index.js
@@ -0,0 +1,5 @@
+import './polyfills.js';
+import '@github/relative-time-element';
+import './origin-url.js';
+import './overflow-menu.js';
+import './absolute-date.js';
diff --git a/web_src/js/webcomponents/GiteaOriginUrl.js b/web_src/js/webcomponents/origin-url.js
similarity index 71%
rename from web_src/js/webcomponents/GiteaOriginUrl.js
rename to web_src/js/webcomponents/origin-url.js
index 5d71d95c60..09aa77f2c0 100644
--- a/web_src/js/webcomponents/GiteaOriginUrl.js
+++ b/web_src/js/webcomponents/origin-url.js
@@ -1,7 +1,8 @@
-// Convert an absolute or relative URL to an absolute URL with the current origin
+// Convert an absolute or relative URL to an absolute URL with the current origin. It only
+// processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'.
+// NOTE: Keep this function in sync with clone_script.tmpl
 export function toOriginUrl(urlStr) {
   try {
-    // only process absolute HTTP/HTTPS URL or relative URLs ('/xxx' or '//host/xxx')
     if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) {
       const {origin, protocol, hostname, port} = window.location;
       const url = new URL(urlStr, origin);
@@ -14,7 +15,7 @@ export function toOriginUrl(urlStr) {
   return urlStr;
 }
 
-window.customElements.define('gitea-origin-url', class extends HTMLElement {
+window.customElements.define('origin-url', class extends HTMLElement {
   connectedCallback() {
     this.textContent = toOriginUrl(this.getAttribute('data-url'));
   }
diff --git a/web_src/js/webcomponents/GiteaOriginUrl.test.js b/web_src/js/webcomponents/origin-url.test.js
similarity index 94%
rename from web_src/js/webcomponents/GiteaOriginUrl.test.js
rename to web_src/js/webcomponents/origin-url.test.js
index f0629842b8..3b2ab89f2a 100644
--- a/web_src/js/webcomponents/GiteaOriginUrl.test.js
+++ b/web_src/js/webcomponents/origin-url.test.js
@@ -1,4 +1,4 @@
-import {toOriginUrl} from './GiteaOriginUrl.js';
+import {toOriginUrl} from './origin-url.js';
 
 test('toOriginUrl', () => {
   const oldLocation = window.location;
diff --git a/web_src/js/webcomponents/overflow-menu.js b/web_src/js/webcomponents/overflow-menu.js
new file mode 100644
index 0000000000..604fce7d4b
--- /dev/null
+++ b/web_src/js/webcomponents/overflow-menu.js
@@ -0,0 +1,198 @@
+import {throttle} from 'throttle-debounce';
+import {createTippy} from '../modules/tippy.js';
+import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
+import octiconKebabHorizontal from '../../../public/assets/img/svg/octicon-kebab-horizontal.svg';
+
+window.customElements.define('overflow-menu', class extends HTMLElement {
+  updateItems = throttle(100, () => {
+    if (!this.tippyContent) {
+      const div = document.createElement('div');
+      div.classList.add('tippy-target');
+      div.tabIndex = '-1'; // for initial focus, programmatic focus only
+      div.addEventListener('keydown', (e) => {
+        if (e.key === 'Tab') {
+          const items = this.tippyContent.querySelectorAll('[role="menuitem"]');
+          if (e.shiftKey) {
+            if (document.activeElement === items[0]) {
+              e.preventDefault();
+              items[items.length - 1].focus();
+            }
+          } else {
+            if (document.activeElement === items[items.length - 1]) {
+              e.preventDefault();
+              items[0].focus();
+            }
+          }
+        } else if (e.key === 'Escape') {
+          e.preventDefault();
+          e.stopPropagation();
+          this.button._tippy.hide();
+          this.button.focus();
+        } else if (e.key === ' ' || e.code === 'Enter') {
+          if (document.activeElement?.matches('[role="menuitem"]')) {
+            e.preventDefault();
+            e.stopPropagation();
+            document.activeElement.click();
+          }
+        } else if (e.key === 'ArrowDown') {
+          if (document.activeElement?.matches('.tippy-target')) {
+            e.preventDefault();
+            e.stopPropagation();
+            document.activeElement.querySelector('[role="menuitem"]:first-of-type').focus();
+          } else if (document.activeElement?.matches('[role="menuitem"]')) {
+            e.preventDefault();
+            e.stopPropagation();
+            document.activeElement.nextElementSibling?.focus();
+          }
+        } else if (e.key === 'ArrowUp') {
+          if (document.activeElement?.matches('.tippy-target')) {
+            e.preventDefault();
+            e.stopPropagation();
+            document.activeElement.querySelector('[role="menuitem"]:last-of-type').focus();
+          } else if (document.activeElement?.matches('[role="menuitem"]')) {
+            e.preventDefault();
+            e.stopPropagation();
+            document.activeElement.previousElementSibling?.focus();
+          }
+        }
+      });
+      this.append(div);
+      this.tippyContent = div;
+    }
+
+    // move items in tippy back into the menu items for subsequent measurement
+    for (const item of this.tippyItems || []) {
+      this.menuItemsEl.append(item);
+    }
+
+    // measure which items are partially outside the element and move them into the button menu
+    this.tippyItems = [];
+    const menuRight = this.offsetLeft + this.offsetWidth;
+    const menuItems = this.menuItemsEl.querySelectorAll('.item');
+    for (const item of menuItems) {
+      const itemRight = item.offsetLeft + item.offsetWidth;
+      if (menuRight - itemRight < 38) { // roughly the width of .overflow-menu-button
+        this.tippyItems.push(item);
+      }
+    }
+
+    // if there are no overflown items, remove any previously created button
+    if (!this.tippyItems?.length) {
+      const btn = this.querySelector('.overflow-menu-button');
+      btn?._tippy?.destroy();
+      btn?.remove();
+      return;
+    }
+
+    // remove aria role from items that moved from tippy to menu
+    for (const item of menuItems) {
+      if (!this.tippyItems.includes(item)) {
+        item.removeAttribute('role');
+      }
+    }
+
+    // move all items that overflow into tippy
+    for (const item of this.tippyItems) {
+      item.setAttribute('role', 'menuitem');
+      this.tippyContent.append(item);
+    }
+
+    // update existing tippy
+    if (this.button?._tippy) {
+      this.button._tippy.setContent(this.tippyContent);
+      return;
+    }
+
+    // create button initially
+    const btn = document.createElement('button');
+    btn.classList.add('overflow-menu-button', 'btn', 'tw-px-2', 'hover:tw-text-text-dark');
+    btn.setAttribute('aria-label', window.config.i18n.more_items);
+    btn.innerHTML = octiconKebabHorizontal;
+    this.append(btn);
+    this.button = btn;
+
+    createTippy(btn, {
+      trigger: 'click',
+      hideOnClick: true,
+      interactive: true,
+      placement: 'bottom-end',
+      role: 'menu',
+      content: this.tippyContent,
+      onShow: () => { // FIXME: onShown doesn't work (never be called)
+        setTimeout(() => {
+          this.tippyContent.focus();
+        }, 0);
+      },
+    });
+  });
+
+  init() {
+    // for horizontal menus where fomantic boldens active items, prevent this bold text from
+    // enlarging the menu's active item replacing the text node with a div that renders a
+    // invisible pseudo-element that enlarges the box.
+    if (this.matches('.ui.secondary.pointing.menu, .ui.tabular.menu')) {
+      for (const item of this.querySelectorAll('.item')) {
+        for (const child of item.childNodes) {
+          if (child.nodeType === Node.TEXT_NODE) {
+            const text = child.textContent.trim(); // whitespace is insignificant inside flexbox
+            if (!text) continue;
+            const span = document.createElement('span');
+            span.classList.add('resize-for-semibold');
+            span.setAttribute('data-text', text);
+            span.textContent = text;
+            child.replaceWith(span);
+          }
+        }
+      }
+    }
+
+    // ResizeObserver triggers on initial render, so we don't manually call `updateItems` here which
+    // also avoids a full-page FOUC in Firefox that happens when `updateItems` is called too soon.
+    this.resizeObserver = new ResizeObserver((entries) => {
+      for (const entry of entries) {
+        const newWidth = entry.contentBoxSize[0].inlineSize;
+        if (newWidth !== this.lastWidth) {
+          requestAnimationFrame(() => {
+            this.updateItems();
+          });
+          this.lastWidth = newWidth;
+        }
+      }
+    });
+    this.resizeObserver.observe(this);
+  }
+
+  connectedCallback() {
+    this.setAttribute('role', 'navigation');
+
+    // check whether the mandatory `.overflow-menu-items` element is present initially which happens
+    // with Vue which renders differently than browsers. If it's not there, like in the case of browser
+    // template rendering, wait for its addition.
+    // The eslint rule is not sophisticated enough or aware of this problem, see
+    // https://github.com/43081j/eslint-plugin-wc/pull/130
+    const menuItemsEl = this.querySelector('.overflow-menu-items'); // eslint-disable-line wc/no-child-traversal-in-connectedcallback
+    if (menuItemsEl) {
+      this.menuItemsEl = menuItemsEl;
+      this.init();
+    } else {
+      this.mutationObserver = new MutationObserver((mutations) => {
+        for (const mutation of mutations) {
+          for (const node of mutation.addedNodes) {
+            if (!isDocumentFragmentOrElementNode(node)) continue;
+            if (node.classList.contains('overflow-menu-items')) {
+              this.menuItemsEl = node;
+              this.mutationObserver?.disconnect();
+              this.init();
+            }
+          }
+        }
+      });
+      this.mutationObserver.observe(this, {childList: true});
+    }
+  }
+
+  disconnectedCallback() {
+    this.mutationObserver?.disconnect();
+    this.resizeObserver?.disconnect();
+  }
+});
diff --git a/web_src/js/webcomponents/polyfill.js b/web_src/js/webcomponents/polyfills.js
similarity index 98%
rename from web_src/js/webcomponents/polyfill.js
rename to web_src/js/webcomponents/polyfills.js
index 88c7276881..38f50fa02f 100644
--- a/web_src/js/webcomponents/polyfill.js
+++ b/web_src/js/webcomponents/polyfills.js
@@ -9,7 +9,7 @@ try {
       return {
         format(value) {
           return ` ${value} ${options.unit}`;
-        }
+        },
       };
     }
     return intlNumberFormat(locales, options);
diff --git a/web_src/js/webcomponents/webcomponents.js b/web_src/js/webcomponents/webcomponents.js
deleted file mode 100644
index 916a588db6..0000000000
--- a/web_src/js/webcomponents/webcomponents.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import '@webcomponents/custom-elements'; // polyfill for some browsers like PaleMoon
-import './polyfill.js';
-
-import '@github/relative-time-element';
-import './GiteaOriginUrl.js';
diff --git a/web_src/svg/gitea-bitbucket.svg b/web_src/svg/gitea-bitbucket.svg
index d3b15a9dc6..ac490c944f 100644
--- a/web_src/svg/gitea-bitbucket.svg
+++ b/web_src/svg/gitea-bitbucket.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 295" class="svg gitea-bitbucket" width="16" height="16" aria-hidden="true"><g fill="#205081"><path d="M128 0C57.732 0 .012 18.822.012 42.663c0 6.274 15.057 95.364 21.331 130.498 2.51 16.312 43.918 38.898 106.657 38.898 62.74 0 102.893-22.586 106.657-38.898 6.274-35.134 21.331-124.224 21.331-130.498C254.734 18.822 198.268 0 128 0zm0 183.199c-22.586 0-40.153-17.567-40.153-40.153s17.567-40.153 40.153-40.153 40.153 17.567 40.153 40.153c0 21.331-17.567 40.153-40.153 40.153zm0-127.988c-45.172 0-81.561-7.53-81.561-17.567 0-10.039 36.389-17.567 81.561-17.567 45.172 0 81.561 7.528 81.561 17.567 0 10.038-36.389 17.567-81.561 17.567z"/><path d="M220.608 207.04c-2.51 0-3.764 1.255-3.764 1.255s-31.37 25.096-87.835 25.096c-56.466 0-87.835-25.096-87.835-25.096s-2.51-1.255-3.765-1.255c-2.51 0-5.019 1.255-5.019 5.02v1.254c5.02 26.35 8.784 45.172 8.784 47.682 3.764 18.822 41.408 33.88 86.58 33.88s82.816-15.058 86.58-33.88c0-2.51 3.765-21.332 8.784-47.682v-1.255c1.255-2.51 0-5.019-2.51-5.019z"/><circle cx="128" cy="141.791" r="20.077"/></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.42 62.42"><defs><linearGradient id="a" x1="64.01" x2="32.99" y1="30.27" y2="54.48" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient></defs><g data-name="Layer 2"><path fill="#2684ff" d="M2 3.13a2 2 0 0 0-2 2.32l8.49 51.54a2.72 2.72 0 0 0 2.66 2.27h40.73a2 2 0 0 0 2-1.68l8.49-52.12a2 2 0 0 0-2-2.32Zm35.75 37.25h-13l-3.52-18.39H40.9Z"/><path fill="url(#a)" d="M59.67 25.12H40.9l-3.15 18.39h-13L9.4 61.73a2.71 2.71 0 0 0 1.75.66h40.74a2 2 0 0 0 2-1.68Z" transform="translate(0 -3.13)"/></g></svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-discord.svg b/web_src/svg/gitea-discord.svg
index ea64a39f6e..4cadbc7f7e 100644
--- a/web_src/svg/gitea-discord.svg
+++ b/web_src/svg/gitea-discord.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" viewBox="0 0 256 293" class="svg gitea-discord" width="16" height="16" aria-hidden="true"><path fill="#7289DA" d="M226.011 0H29.99C13.459 0 0 13.458 0 30.135v197.778c0 16.677 13.458 30.135 29.989 30.135h165.888l-7.754-27.063 18.725 17.408 17.7 16.384L256 292.571V30.135C256 13.458 242.542 0 226.011 0zm-56.466 191.05s-5.266-6.291-9.655-11.85c19.164-5.413 26.478-17.408 26.478-17.408-5.998 3.95-11.703 6.73-16.823 8.63-7.314 3.073-14.336 5.12-21.211 6.291-14.044 2.633-26.917 1.902-37.888-.146-8.339-1.61-15.507-3.95-21.504-6.29-3.365-1.317-7.022-2.926-10.68-4.974-.438-.293-.877-.439-1.316-.732a2.022 2.022 0 0 1-.585-.438c-2.633-1.463-4.096-2.487-4.096-2.487s7.022 11.703 25.6 17.261c-4.388 5.56-9.801 12.142-9.801 12.142-32.33-1.024-44.617-22.235-44.617-22.235 0-47.104 21.065-85.285 21.065-85.285 21.065-15.799 41.106-15.36 41.106-15.36l1.463 1.756C80.75 77.53 68.608 89.088 68.608 89.088s3.218-1.755 8.63-4.242c15.653-6.876 28.088-8.777 33.208-9.216.877-.147 1.609-.293 2.487-.293a123.776 123.776 0 0 1 29.55-.292c13.896 1.609 28.818 5.705 44.031 14.043 0 0-11.556-10.971-36.425-18.578l2.048-2.34s20.041-.44 41.106 15.36c0 0 21.066 38.18 21.066 85.284 0 0-12.435 21.211-44.764 22.235zm-68.023-68.316c-8.338 0-14.92 7.314-14.92 16.237 0 8.924 6.728 16.238 14.92 16.238 8.339 0 14.921-7.314 14.921-16.238.147-8.923-6.582-16.237-14.92-16.237m53.394 0c-8.339 0-14.922 7.314-14.922 16.237 0 8.924 6.73 16.238 14.922 16.238 8.338 0 14.92-7.314 14.92-16.238 0-8.923-6.582-16.237-14.92-16.237"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><path fill="#5865f2" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-facebook.svg b/web_src/svg/gitea-facebook.svg
index 8163e2a966..68cd20750a 100644
--- a/web_src/svg/gitea-facebook.svg
+++ b/web_src/svg/gitea-facebook.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" style="shape-rendering:geometricPrecision;text-rendering:geometricPrecision;image-rendering:optimizeQuality;fill-rule:evenodd;clip-rule:evenodd" viewBox="0 0 128 128" class="svg gitea-facebook" width="16" height="16" aria-hidden="true"><path fill="#395b97" d="M93.5 8.5c-1.452.802-3.118 1.302-5 1.5L10 88.5c-.198 1.882-.698 3.548-1.5 5a551.581 551.581 0 0 1-.5-56c2.5-17.167 12.333-27 29.5-29.5a551.581 551.581 0 0 1 56 .5Z" style="opacity:.995"/><path fill="#366098" d="M93.5 8.5c15.888 4.225 24.555 14.558 26 31a676.749 676.749 0 0 0-1.5 37l-35 35a32.438 32.438 0 0 0-.5 8 441.615 441.615 0 0 1-1-42h14a379.883 379.883 0 0 0 3-17h-17c-2.5-13.83 3.166-19.83 17-18v-16c-25.755-3.243-36.755 8.09-33 34h-14v17h14v42c-9.34.166-18.673 0-28-.5-15.451-1.953-25.118-10.453-29-25.5.802-1.452 1.302-3.118 1.5-5L88.5 10c1.882-.198 3.548-.698 5-1.5Z" style="opacity:.976"/><path fill="#346499" d="M119.5 39.5c.167 16.67 0 33.337-.5 50-3.622 20.245-15.788 30.245-36.5 30a32.438 32.438 0 0 1 .5-8l35-35c.169-12.507.669-24.84 1.5-37Z" style="opacity:.918"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" clip-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="0 0 14222 14222"><g fill-rule="nonzero"><path fill="#1977f3" d="M14222 7111C14222 3184 11038 0 7111 0S0 3184 0 7111c0 3549 2600 6491 6000 7025V9167H4194V7111h1806V5544c0-1782 1062-2767 2686-2767 778 0 1592 139 1592 139v1750h-897c-883 0-1159 548-1159 1111v1334h1972l-315 2056H8222v4969c3400-533 6000-3475 6000-7025z"/><path fill="#fefefe" d="m9879 9167 315-2056H8222V5777c0-562 275-1111 1159-1111h897V2916s-814-139-1592-139c-1624 0-2686 984-2686 2767v1567H4194v2056h1806v4969c362 57 733 86 1111 86s749-30 1111-86V9167h1657z"/></g></svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-jetbrains.svg b/web_src/svg/gitea-jetbrains.svg
new file mode 100644
index 0000000000..a7884c4289
--- /dev/null
+++ b/web_src/svg/gitea-jetbrains.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
+<g>
+	<g>
+		<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0.7898" y1="40.0893" x2="33.3172" y2="40.0893">
+			<stop  offset="0.2581" style="stop-color:#F97A12"/>
+      <stop  offset="0.4591" style="stop-color:#B07B58"/>
+      <stop  offset="0.7241" style="stop-color:#577BAE"/>
+      <stop  offset="0.9105" style="stop-color:#1E7CE5"/>
+      <stop  offset="1" style="stop-color:#087CFA"/>
+		</linearGradient>
+    <polygon style="fill:url(#SVGID_1_);" points="17.7,54.6 0.8,41.2 9.2,25.6 33.3,35 		"/>
+    <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="25.7674" y1="24.88" x2="79.424" y2="54.57">
+			<stop  offset="0" style="stop-color:#F97A12"/>
+      <stop  offset="7.179946e-002" style="stop-color:#CB7A3E"/>
+      <stop  offset="0.1541" style="stop-color:#9E7B6A"/>
+      <stop  offset="0.242" style="stop-color:#757B91"/>
+      <stop  offset="0.3344" style="stop-color:#537BB1"/>
+      <stop  offset="0.4324" style="stop-color:#387CCC"/>
+      <stop  offset="0.5381" style="stop-color:#237CE0"/>
+      <stop  offset="0.6552" style="stop-color:#147CEF"/>
+      <stop  offset="0.7925" style="stop-color:#0B7CF7"/>
+      <stop  offset="1" style="stop-color:#087CFA"/>
+		</linearGradient>
+    <polygon style="fill:url(#SVGID_2_);" points="70,18.7 68.7,59.2 41.8,70 25.6,59.6 49.3,35 38.9,12.3 48.2,1.1 		"/>
+    <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="63.2277" y1="42.9153" x2="48.2903" y2="-1.7191">
+			<stop  offset="0" style="stop-color:#FE315D"/>
+      <stop  offset="7.840246e-002" style="stop-color:#CB417E"/>
+      <stop  offset="0.1601" style="stop-color:#9E4E9B"/>
+      <stop  offset="0.2474" style="stop-color:#755BB4"/>
+      <stop  offset="0.3392" style="stop-color:#5365CA"/>
+      <stop  offset="0.4365" style="stop-color:#386DDB"/>
+      <stop  offset="0.5414" style="stop-color:#2374E9"/>
+      <stop  offset="0.6576" style="stop-color:#1478F3"/>
+      <stop  offset="0.794" style="stop-color:#0B7BF8"/>
+      <stop  offset="1" style="stop-color:#087CFA"/>
+		</linearGradient>
+    <polygon style="fill:url(#SVGID_3_);" points="70,18.7 48.7,43.9 38.9,12.3 48.2,1.1 		"/>
+    <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="10.7204" y1="16.473" x2="55.5237" y2="90.58">
+			<stop  offset="0" style="stop-color:#FE315D"/>
+      <stop  offset="4.023279e-002" style="stop-color:#F63462"/>
+      <stop  offset="0.1037" style="stop-color:#DF3A71"/>
+      <stop  offset="0.1667" style="stop-color:#C24383"/>
+      <stop  offset="0.2912" style="stop-color:#AD4A91"/>
+      <stop  offset="0.5498" style="stop-color:#755BB4"/>
+      <stop  offset="0.9175" style="stop-color:#1D76ED"/>
+      <stop  offset="1" style="stop-color:#087CFA"/>
+		</linearGradient>
+    <polygon style="fill:url(#SVGID_4_);" points="33.7,58.1 5.6,68.3 10.1,52.5 16,33.1 0,27.7 10.1,0 32.1,2.7 53.7,27.4 		"/>
+	</g>
+  <g>
+		<rect x="13.7" y="13.5" style="fill:#000000;" width="43.2" height="43.2"/>
+    <rect x="17.7" y="48.6" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
+    <polygon style="fill:#FFFFFF;" points="29.4,22.4 29.4,19.1 20.4,19.1 20.4,22.4 23,22.4 23,33.7 20.4,33.7 20.4,37 29.4,37
+			29.4,33.7 26.9,33.7 26.9,22.4 		"/>
+    <path style="fill:#FFFFFF;" d="M38,37.3c-1.4,0-2.6-0.3-3.5-0.8c-0.9-0.5-1.7-1.2-2.3-1.9l2.5-2.8c0.5,0.6,1,1,1.5,1.3
+			c0.5,0.3,1.1,0.5,1.7,0.5c0.7,0,1.3-0.2,1.8-0.7c0.4-0.5,0.6-1.2,0.6-2.3V19.1h4v11.7c0,1.1-0.1,2-0.4,2.8c-0.3,0.8-0.7,1.4-1.3,2
+			c-0.5,0.5-1.2,1-2,1.2C39.8,37.1,39,37.3,38,37.3"/>
+	</g>
+</g>
+</svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-microsoftonline.svg b/web_src/svg/gitea-microsoftonline.svg
index 72ef94eabb..eb28296419 100644
--- a/web_src/svg/gitea-microsoftonline.svg
+++ b/web_src/svg/gitea-microsoftonline.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2075 2499.8" class="svg gitea-microsoftonline" width="16" height="16" aria-hidden="true"><path fill="#eb3c00" d="M0 2016.6V496.8L1344.4 0 2075 233.7v2045.9l-730.6 220.3L0 2016.6l1344.4 161.8V409.2L467.6 613.8v1198.3z"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48"><path fill="url(#a)" d="m20.084 3.026-.224.136a8.007 8.007 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258c.074-.045.149-.089.224-.131Z"/><path fill="url(#b)" d="m20.084 3.026-.224.136a8.007 8.007 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258c.074-.045.149-.089.224-.131Z"/><path fill="url(#c)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26l-11-7Z"/><path fill="url(#d)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26l-11-7Z"/><path fill="url(#e)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31c.003-.088.004-.175.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#f)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31c.003-.088.004-.175.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#g)" d="M4.004 30.998Z"/><path fill="url(#h)" d="M4.004 30.998Z"/><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="translate(17.4186 10.6383) rotate(110.528) scale(33.3657 58.1966)" gradientUnits="userSpaceOnUse"><stop offset=".064" stop-color="#AE7FE2"/><stop offset="1" stop-color="#0078D4"/></radialGradient><radialGradient id="c" cx="0" cy="0" r="1" gradientTransform="translate(10.4299 36.3511) rotate(-8.36717) scale(31.0503 20.5108)" gradientUnits="userSpaceOnUse"><stop offset=".134" stop-color="#D59DFF"/><stop offset="1" stop-color="#5E438F"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="translate(41.0552 26.504) rotate(-165.772) scale(24.9228 41.9552)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><radialGradient id="g" cx="0" cy="0" r="1" gradientTransform="translate(41.0552 26.504) rotate(-165.772) scale(24.9228 41.9552)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><linearGradient id="b" x1="17.512" x2="12.751" y1="37.868" y2="29.635" gradientUnits="userSpaceOnUse"><stop stop-color="#114A8B"/><stop offset="1" stop-color="#0078D4" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="40.357" x2="35.255" y1="25.377" y2="32.692" gradientUnits="userSpaceOnUse"><stop stop-color="#493474"/><stop offset="1" stop-color="#8C66BA" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient><linearGradient id="h" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient></defs></svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-twitter.svg b/web_src/svg/gitea-twitter.svg
index 096b9add2b..f972d23f90 100644
--- a/web_src/svg/gitea-twitter.svg
+++ b/web_src/svg/gitea-twitter.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="-89.009 -46.884 643.937 446.884" class="svg gitea-twitter" width="16" height="16" aria-hidden="true"><path fill="#1da1f2" fill-rule="nonzero" d="M154.729 400c185.669 0 287.205-153.876 287.205-287.312 0-4.37-.089-8.72-.286-13.052A205.304 205.304 0 0 0 492 47.346c-18.087 8.044-37.55 13.458-57.968 15.899 20.841-12.501 36.84-32.278 44.389-55.852a202.42 202.42 0 0 1-64.098 24.511C395.903 12.276 369.679 0 340.641 0c-55.744 0-100.948 45.222-100.948 100.965 0 7.925.887 15.631 2.619 23.025-83.895-4.223-158.287-44.405-208.074-105.504A100.739 100.739 0 0 0 20.57 69.24c0 35.034 17.82 65.961 44.92 84.055a100.172 100.172 0 0 1-45.716-12.63c-.015.424-.015.837-.015 1.29 0 48.903 34.794 89.734 80.982 98.986a101.036 101.036 0 0 1-26.617 3.553c-6.493 0-12.821-.639-18.971-1.82 12.851 40.122 50.115 69.319 94.296 70.135-34.549 27.089-78.07 43.224-125.371 43.224A204.9 204.9 0 0 1 0 354.634c44.674 28.645 97.72 45.359 154.734 45.359"/></svg>
\ No newline at end of file
+<svg viewBox="0 0 24 24"><path d="M14.095 10.316 22.286 1h-1.94L13.23 9.088 7.551 1H1l8.59 12.231L1 23h1.94l7.51-8.543 6 8.543H23l-8.905-12.684zm-2.658 3.022-.872-1.218L3.64 2.432h2.98l5.59 7.821.869 1.219 7.265 10.166h-2.982l-5.926-8.3z"/></svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-vscodium.svg b/web_src/svg/gitea-vscodium.svg
new file mode 100644
index 0000000000..483676fe71
--- /dev/null
+++ b/web_src/svg/gitea-vscodium.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="100%" height="100%" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" version="1.1" viewBox="0 0 16 16"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9a1046.4 1046.4 0 0 0 .8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3 .2 1.2 0 2.5-.2 3.7 0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8.2.4.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg>
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
index 16afa0ff9c..fdf80a5313 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -11,6 +11,10 @@ import webpack from 'webpack';
 import {fileURLToPath} from 'node:url';
 import {readFileSync} from 'node:fs';
 import {env} from 'node:process';
+import tailwindcss from 'tailwindcss';
+import tailwindConfig from './tailwind.config.js';
+import tailwindcssNesting from 'tailwindcss/nesting/index.js';
+import postcssNesting from 'postcss-nesting';
 
 const {EsbuildPlugin} = EsBuildLoader;
 const {SourceMapDevToolPlugin, DefinePlugin} = webpack;
@@ -39,6 +43,18 @@ if ('ENABLE_SOURCEMAP' in env) {
   sourceMaps = isProduction ? 'reduced' : 'true';
 }
 
+// define which web components we use for Vue to not interpret them as Vue components
+const webComponents = new Set([
+  // our own, in web_src/js/webcomponents
+  'overflow-menu',
+  'origin-url',
+  'absolute-date',
+  // from dependencies
+  'markdown-toolbar',
+  'relative-time',
+  'text-expander',
+]);
+
 const filterCssImport = (url, ...args) => {
   const cssFile = args[1] || args[0]; // resourcePath is 2nd argument for url and 3rd for import
   const importedFile = url.replace(/[?#].+/, '').toLowerCase();
@@ -68,7 +84,7 @@ export default {
       fileURLToPath(new URL('web_src/css/index.css', import.meta.url)),
     ],
     webcomponents: [
-      fileURLToPath(new URL('web_src/js/webcomponents/webcomponents.js', import.meta.url)),
+      fileURLToPath(new URL('web_src/js/webcomponents/index.js', import.meta.url)),
     ],
     swagger: [
       fileURLToPath(new URL('web_src/js/standalone/swagger.js', import.meta.url)),
@@ -117,6 +133,11 @@ export default {
         test: /\.vue$/i,
         exclude: /node_modules/,
         loader: 'vue-loader',
+        options: {
+          compilerOptions: {
+            isCustomElement: (tag) => webComponents.has(tag),
+          },
+        },
       },
       {
         test: /\.js$/i,
@@ -143,6 +164,18 @@ export default {
               sourceMap: sourceMaps === 'true',
               url: {filter: filterCssImport},
               import: {filter: filterCssImport},
+              importLoaders: 1,
+            },
+          },
+          {
+            loader: 'postcss-loader',
+            options: {
+              postcssOptions: {
+                plugins: [
+                  tailwindcssNesting(postcssNesting({edition: '2024-02'})),
+                  tailwindcss(tailwindConfig),
+                ],
+              },
             },
           },
         ],
@@ -157,21 +190,18 @@ export default {
         type: 'asset/resource',
         generator: {
           filename: 'fonts/[name].[contenthash:8][ext]',
-        }
-      },
-      {
-        test: /\.png$/i,
-        type: 'asset/resource',
-        generator: {
-          filename: 'img/webpack/[name].[contenthash:8][ext]',
-        }
+        },
       },
     ],
   },
   plugins: [
+    new webpack.ProvidePlugin({ // for htmx extensions
+      htmx: 'htmx.org',
+    }),
     new DefinePlugin({
       __VUE_OPTIONS_API__: true, // at the moment, many Vue components still use the Vue Options API
       __VUE_PROD_DEVTOOLS__: false, // do not enable devtools support in production
+      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, // https://github.com/vuejs/vue-cli/pull/7443
     }),
     new VueLoaderPlugin(),
     new MiniCssExtractPlugin({
@@ -205,10 +235,10 @@ export default {
       },
       override: {
         'khroma@*': {licenseName: 'MIT'}, // https://github.com/fabiospampinato/khroma/pull/33
-        'htmx.org@1.9.10': {licenseName: 'BSD-2-Clause'}, // "BSD 2-Clause" -> "BSD-2-Clause"
+        'idiomorph@0.3.0': {licenseName: 'BSD-2-Clause'}, // https://github.com/bigskysoftware/idiomorph/pull/37
       },
       emitError: true,
-      allow: '(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT OR ISC OR CPAL-1.0 OR Unlicense OR EPL-1.0 OR EPL-2.0)',
+      allow: '(Apache-2.0 OR 0BSD OR BSD-2-Clause OR BSD-3-Clause OR MIT OR ISC OR CPAL-1.0 OR Unlicense OR EPL-1.0 OR EPL-2.0)',
     }) : new AddAssetPlugin('licenses.txt', `Licenses are disabled during development`),
   ],
   performance: {