build(web)!: upgrade to tailwind css v4

Quentin Gliech and Claude Opus 4.6 (1M context) created

migrate from tailwind v3 to v4:
- replace postcss-based setup with @tailwindcss/vite plugin
- rewrite index.css: @tailwind directives β†’ @import "tailwindcss",
  JS theme config β†’ CSS @theme inline block
- replace tailwindcss-animate with tw-animate-css
- load @tailwindcss/typography via @plugin directive
- configure dark mode via @custom-variant
- delete tailwind.config.ts and postcss.config.js

rename classes per v4 spec:
- shadow β†’ shadow-sm, shadow-sm β†’ shadow-xs
- rounded β†’ rounded-sm
- outline-none β†’ outline-hidden

also fix type errors from apollo v4 (add explicit query types),
react-router v7 (void navigate promises), and react-markdown v10
(move className to wrapper div)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Change summary

webui2/package.json                           |   9 
webui2/pnpm-lock.yaml                         | 589 +++++++-------------
webui2/postcss.config.js                      |   6 
webui2/src/components/bugs/BugRow.tsx         |   8 
webui2/src/components/bugs/CommentBox.tsx     |  10 
webui2/src/components/bugs/IssueFilters.tsx   |  48 
webui2/src/components/bugs/LabelEditor.tsx    |   8 
webui2/src/components/bugs/QueryInput.tsx     |  10 
webui2/src/components/bugs/Timeline.tsx       |  22 
webui2/src/components/bugs/TitleEditor.tsx    |   6 
webui2/src/components/code/CodeBreadcrumb.tsx |   8 
webui2/src/components/code/CommitList.tsx     |  44 
webui2/src/components/code/FileDiffView.tsx   |  39 
webui2/src/components/code/FileTree.tsx       |  28 
webui2/src/components/code/FileViewer.tsx     |  12 
webui2/src/components/code/RefSelector.tsx    |  18 
webui2/src/components/content/Markdown.tsx    |  27 
webui2/src/components/layout/Header.tsx       |   6 
webui2/src/components/layout/Shell.tsx        |   2 
webui2/src/components/ui/badge.tsx            |   7 
webui2/src/components/ui/button.tsx           |  10 
webui2/src/components/ui/input.tsx            |   2 
webui2/src/components/ui/popover.tsx          |   2 
webui2/src/components/ui/textarea.tsx         |   2 
webui2/src/index.css                          | 160 +++-
webui2/src/lib/auth.tsx                       |   4 
webui2/src/pages/BugDetailPage.tsx            |  14 
webui2/src/pages/BugListPage.tsx              |  22 
webui2/src/pages/CodePage.tsx                 |  41 +
webui2/src/pages/CommitPage.tsx               |  56 +
webui2/src/pages/ErrorPage.tsx                |   4 
webui2/src/pages/IdentitySelectPage.tsx       |  28 
webui2/src/pages/NewBugPage.tsx               |  16 
webui2/src/pages/RepoPickerPage.tsx           |  16 
webui2/src/pages/UserProfilePage.tsx          |  44 
webui2/tailwind.config.ts                     |  71 --
webui2/tsconfig.node.json                     |   2 
webui2/vite.config.ts                         |   3 
38 files changed, 644 insertions(+), 760 deletions(-)

Detailed changes

webui2/package.json πŸ”—

@@ -40,23 +40,22 @@
     "remark-gfm": "^4.0.0",
     "rxjs": "^7.8.2",
     "tailwind-merge": "^3.5.0",
-    "tailwindcss-animate": "^1.0.7"
+    "tw-animate-css": "^1.4.0"
   },
   "devDependencies": {
     "@graphql-codegen/cli": "^6.2.1",
     "@graphql-codegen/typescript": "^5.0.9",
     "@graphql-codegen/typescript-operations": "^5.0.9",
     "@graphql-codegen/typescript-react-apollo": "^4.3.2",
-    "@tailwindcss/typography": "^0.5.15",
+    "@tailwindcss/typography": "^0.5.19",
+    "@tailwindcss/vite": "^4.2.2",
     "@types/react": "^19.1.0",
     "@types/react-dom": "^19.1.0",
     "@vitejs/plugin-react": "^6.0.1",
-    "autoprefixer": "^10.4.20",
     "oxfmt": "^0.42.0",
     "oxlint": "^1.57.0",
     "oxlint-tsgolint": "^0.18.1",
-    "postcss": "^8.4.49",
-    "tailwindcss": "^3.4.17",
+    "tailwindcss": "^4.2.2",
     "typescript": "^6.0.2",
     "vite": "^8.0.3"
   },

webui2/pnpm-lock.yaml πŸ”—

@@ -83,9 +83,9 @@ importers:
       tailwind-merge:
         specifier: ^3.5.0
         version: 3.5.0
-      tailwindcss-animate:
-        specifier: ^1.0.7
-        version: 1.0.7(tailwindcss@3.4.19(yaml@2.8.3))
+      tw-animate-css:
+        specifier: ^1.4.0
+        version: 1.4.0
     devDependencies:
       '@graphql-codegen/cli':
         specifier: ^6.2.1
@@ -100,8 +100,11 @@ importers:
         specifier: ^4.3.2
         version: 4.4.1(graphql@16.13.2)
       '@tailwindcss/typography':
-        specifier: ^0.5.15
-        version: 0.5.19(tailwindcss@3.4.19(yaml@2.8.3))
+        specifier: ^0.5.19
+        version: 0.5.19(tailwindcss@4.2.2)
+      '@tailwindcss/vite':
+        specifier: ^4.2.2
+        version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.3))
       '@types/react':
         specifier: ^19.1.0
         version: 19.2.14
@@ -110,10 +113,7 @@ importers:
         version: 19.2.3(@types/react@19.2.14)
       '@vitejs/plugin-react':
         specifier: ^6.0.1
-        version: 6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@1.21.7)(yaml@2.8.3))
-      autoprefixer:
-        specifier: ^10.4.20
-        version: 10.4.27(postcss@8.5.8)
+        version: 6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.3))
       oxfmt:
         specifier: ^0.42.0
         version: 0.42.0
@@ -123,25 +123,18 @@ importers:
       oxlint-tsgolint:
         specifier: ^0.18.1
         version: 0.18.1
-      postcss:
-        specifier: ^8.4.49
-        version: 8.5.8
       tailwindcss:
-        specifier: ^3.4.17
-        version: 3.4.19(yaml@2.8.3)
+        specifier: ^4.2.2
+        version: 4.2.2
       typescript:
         specifier: ^6.0.2
         version: 6.0.2
       vite:
         specifier: ^8.0.3
-        version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@1.21.7)(yaml@2.8.3)
+        version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.3)
 
 packages:
 
-  '@alloc/quick-lru@5.2.0':
-    resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
-    engines: {node: '>=10'}
-
   '@apollo/client@4.1.6':
     resolution: {integrity: sha512-ak8uzqmKeX3u9BziGf83RRyODAJKFkPG72hTNvEj4WjMWFmuKW2gGN1i3OfajKT6yuGjvo+n23ES2zqWDKFCZg==}
     peerDependencies:
@@ -1387,11 +1380,105 @@ packages:
     resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
     engines: {node: '>=10'}
 
+  '@tailwindcss/node@4.2.2':
+    resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
+
+  '@tailwindcss/oxide-android-arm64@4.2.2':
+    resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==}
+    engines: {node: '>= 20'}
+    cpu: [arm64]
+    os: [android]
+
+  '@tailwindcss/oxide-darwin-arm64@4.2.2':
+    resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==}
+    engines: {node: '>= 20'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@tailwindcss/oxide-darwin-x64@4.2.2':
+    resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==}
+    engines: {node: '>= 20'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@tailwindcss/oxide-freebsd-x64@4.2.2':
+    resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==}
+    engines: {node: '>= 20'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
+    resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==}
+    engines: {node: '>= 20'}
+    cpu: [arm]
+    os: [linux]
+
+  '@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
+    resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==}
+    engines: {node: '>= 20'}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
+  '@tailwindcss/oxide-linux-arm64-musl@4.2.2':
+    resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
+    engines: {node: '>= 20'}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
+  '@tailwindcss/oxide-linux-x64-gnu@4.2.2':
+    resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
+    engines: {node: '>= 20'}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
+  '@tailwindcss/oxide-linux-x64-musl@4.2.2':
+    resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
+    engines: {node: '>= 20'}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
+  '@tailwindcss/oxide-wasm32-wasi@4.2.2':
+    resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
+    engines: {node: '>=14.0.0'}
+    cpu: [wasm32]
+    bundledDependencies:
+      - '@napi-rs/wasm-runtime'
+      - '@emnapi/core'
+      - '@emnapi/runtime'
+      - '@tybys/wasm-util'
+      - '@emnapi/wasi-threads'
+      - tslib
+
+  '@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
+    resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==}
+    engines: {node: '>= 20'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@tailwindcss/oxide-win32-x64-msvc@4.2.2':
+    resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==}
+    engines: {node: '>= 20'}
+    cpu: [x64]
+    os: [win32]
+
+  '@tailwindcss/oxide@4.2.2':
+    resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==}
+    engines: {node: '>= 20'}
+
   '@tailwindcss/typography@0.5.19':
     resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
     peerDependencies:
       tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
 
+  '@tailwindcss/vite@4.2.2':
+    resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==}
+    peerDependencies:
+      vite: ^5.2.0 || ^6 || ^7 || ^8
+
   '@tybys/wasm-util@0.10.1':
     resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
 
@@ -1501,16 +1588,6 @@ packages:
     resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
     engines: {node: '>=12'}
 
-  any-promise@1.3.0:
-    resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
-
-  anymatch@3.1.3:
-    resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
-    engines: {node: '>= 8'}
-
-  arg@5.0.2:
-    resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
-
   argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
 
@@ -1526,13 +1603,6 @@ packages:
     resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==}
     engines: {node: '>=8'}
 
-  autoprefixer@10.4.27:
-    resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==}
-    engines: {node: ^10 || ^12 || >=14}
-    hasBin: true
-    peerDependencies:
-      postcss: ^8.1.0
-
   bail@2.0.2:
     resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
 
@@ -1545,10 +1615,6 @@ packages:
     engines: {node: '>=6.0.0'}
     hasBin: true
 
-  binary-extensions@2.3.0:
-    resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
-    engines: {node: '>=8'}
-
   brace-expansion@5.0.5:
     resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
     engines: {node: 18 || 20 || >=22}
@@ -1569,10 +1635,6 @@ packages:
   camel-case@4.1.2:
     resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
 
-  camelcase-css@2.0.1:
-    resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
-    engines: {node: '>= 6'}
-
   caniuse-lite@1.0.30001781:
     resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==}
 
@@ -1611,10 +1673,6 @@ packages:
   chardet@2.1.1:
     resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
 
-  chokidar@3.6.0:
-    resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
-    engines: {node: '>= 8.10.0'}
-
   class-variance-authority@0.7.1:
     resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
 
@@ -1651,10 +1709,6 @@ packages:
   comma-separated-tokens@2.0.3:
     resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
 
-  commander@4.1.1:
-    resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
-    engines: {node: '>= 6'}
-
   common-tags@1.8.2:
     resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
     engines: {node: '>=4.0.0'}
@@ -1747,16 +1801,10 @@ packages:
   devlop@1.1.0:
     resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
 
-  didyoumean@1.2.2:
-    resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
-
   dir-glob@3.0.1:
     resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
     engines: {node: '>=8'}
 
-  dlv@1.1.3:
-    resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
-
   dot-case@3.0.4:
     resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
 
@@ -1775,6 +1823,10 @@ packages:
   emoticon@4.1.0:
     resolution: {integrity: sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==}
 
+  enhanced-resolve@5.20.1:
+    resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
+    engines: {node: '>=10.13.0'}
+
   entities@6.0.1:
     resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
     engines: {node: '>=0.12'}
@@ -1835,17 +1887,11 @@ packages:
     resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
     engines: {node: '>=12.20.0'}
 
-  fraction.js@5.3.4:
-    resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
-
   fsevents@2.3.3:
     resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
     os: [darwin]
 
-  function-bind@1.1.2:
-    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
-
   gensync@1.0.0-beta.2:
     resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
     engines: {node: '>=6.9.0'}
@@ -1869,14 +1915,13 @@ packages:
     resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
     engines: {node: '>= 6'}
 
-  glob-parent@6.0.2:
-    resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
-    engines: {node: '>=10.13.0'}
-
   globby@11.1.0:
     resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
     engines: {node: '>=10'}
 
+  graceful-fs@4.2.11:
+    resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
   graphql-config@5.1.6:
     resolution: {integrity: sha512-fCkYnm4Kdq3un0YIM4BCZHVR5xl0UeLP6syxxO7KAstdY7QVyVvTHP0kRPDYEP1v08uwtJVgis5sj3IOTLOniQ==}
     engines: {node: '>= 16.0.0'}
@@ -1917,10 +1962,6 @@ packages:
     resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
     engines: {node: '>=8'}
 
-  hasown@2.0.2:
-    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
-    engines: {node: '>= 0.4'}
-
   hast-util-from-parse5@8.0.3:
     resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
 
@@ -2009,14 +2050,6 @@ packages:
   is-arrayish@0.2.1:
     resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
 
-  is-binary-path@2.1.0:
-    resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
-    engines: {node: '>=8'}
-
-  is-core-module@2.16.1:
-    resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
-    engines: {node: '>= 0.4'}
-
   is-decimal@2.0.1:
     resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
 
@@ -2079,10 +2112,6 @@ packages:
     peerDependencies:
       ws: '*'
 
-  jiti@1.21.7:
-    resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
-    hasBin: true
-
   jiti@2.6.1:
     resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
     hasBin: true
@@ -2185,10 +2214,6 @@ packages:
     resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
     engines: {node: '>= 12.0.0'}
 
-  lilconfig@3.1.3:
-    resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
-    engines: {node: '>=14'}
-
   lines-and-columns@1.2.4:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
 
@@ -2231,6 +2256,9 @@ packages:
     peerDependencies:
       react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
 
+  magic-string@0.30.21:
+    resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
   map-cache@0.2.2:
     resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==}
     engines: {node: '>=0.10.0'}
@@ -2399,9 +2427,6 @@ packages:
     resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
     engines: {node: ^18.17.0 || >=20.5.0}
 
-  mz@2.7.0:
-    resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
-
   nanoid@3.3.11:
     resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -2430,18 +2455,6 @@ packages:
     resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==}
     engines: {node: '>=0.10.0'}
 
-  normalize-path@3.0.0:
-    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
-    engines: {node: '>=0.10.0'}
-
-  object-assign@4.1.1:
-    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
-    engines: {node: '>=0.10.0'}
-
-  object-hash@3.0.0:
-    resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
-    engines: {node: '>= 6'}
-
   onetime@7.0.0:
     resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
     engines: {node: '>=18'}
@@ -2499,9 +2512,6 @@ packages:
   path-case@3.0.4:
     resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==}
 
-  path-parse@1.0.7:
-    resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
-
   path-root-regex@0.1.2:
     resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==}
     engines: {node: '>=0.10.0'}
@@ -2525,61 +2535,10 @@ packages:
     resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
     engines: {node: '>=12'}
 
-  pify@2.3.0:
-    resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
-    engines: {node: '>=0.10.0'}
-
-  pirates@4.0.7:
-    resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
-    engines: {node: '>= 6'}
-
-  postcss-import@15.1.0:
-    resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
-    engines: {node: '>=14.0.0'}
-    peerDependencies:
-      postcss: ^8.0.0
-
-  postcss-js@4.1.0:
-    resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==}
-    engines: {node: ^12 || ^14 || >= 16}
-    peerDependencies:
-      postcss: ^8.4.21
-
-  postcss-load-config@6.0.1:
-    resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
-    engines: {node: '>= 18'}
-    peerDependencies:
-      jiti: '>=1.21.0'
-      postcss: '>=8.0.9'
-      tsx: ^4.8.1
-      yaml: ^2.4.2
-    peerDependenciesMeta:
-      jiti:
-        optional: true
-      postcss:
-        optional: true
-      tsx:
-        optional: true
-      yaml:
-        optional: true
-
-  postcss-nested@6.2.0:
-    resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
-    engines: {node: '>=12.0'}
-    peerDependencies:
-      postcss: ^8.2.14
-
   postcss-selector-parser@6.0.10:
     resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
     engines: {node: '>=4'}
 
-  postcss-selector-parser@6.1.2:
-    resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
-    engines: {node: '>=4'}
-
-  postcss-value-parser@4.2.0:
-    resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
-
   postcss@8.5.8:
     resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
     engines: {node: ^10 || ^12 || >=14}
@@ -2645,13 +2604,6 @@ packages:
     resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
     engines: {node: '>=0.10.0'}
 
-  read-cache@1.0.0:
-    resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
-
-  readdirp@3.6.0:
-    resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
-    engines: {node: '>=8.10.0'}
-
   rehype-autolink-headings@7.1.0:
     resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==}
 
@@ -2704,11 +2656,6 @@ packages:
     resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
     engines: {node: '>=8'}
 
-  resolve@1.22.11:
-    resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
-    engines: {node: '>= 0.4'}
-    hasBin: true
-
   restore-cursor@5.1.0:
     resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
     engines: {node: '>=18'}
@@ -2816,19 +2763,10 @@ packages:
   style-to-object@1.0.14:
     resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
 
-  sucrase@3.35.1:
-    resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
-    engines: {node: '>=16 || 14 >=14.17'}
-    hasBin: true
-
   supports-color@7.2.0:
     resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
     engines: {node: '>=8'}
 
-  supports-preserve-symlinks-flag@1.0.0:
-    resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
-    engines: {node: '>= 0.4'}
-
   swap-case@2.0.2:
     resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==}
 
@@ -2839,22 +2777,12 @@ packages:
   tailwind-merge@3.5.0:
     resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
 
-  tailwindcss-animate@1.0.7:
-    resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
-    peerDependencies:
-      tailwindcss: '>=3.0.0 || insiders'
-
-  tailwindcss@3.4.19:
-    resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==}
-    engines: {node: '>=14.0.0'}
-    hasBin: true
-
-  thenify-all@1.6.0:
-    resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
-    engines: {node: '>=0.8'}
+  tailwindcss@4.2.2:
+    resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
 
-  thenify@3.3.1:
-    resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+  tapable@2.3.2:
+    resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
+    engines: {node: '>=6'}
 
   timeout-signal@2.0.0:
     resolution: {integrity: sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==}
@@ -2881,9 +2809,6 @@ packages:
   trough@2.2.0:
     resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
 
-  ts-interface-checker@0.1.13:
-    resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
-
   ts-log@2.2.7:
     resolution: {integrity: sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==}
 
@@ -2893,6 +2818,9 @@ packages:
   tslib@2.8.1:
     resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
 
+  tw-animate-css@1.4.0:
+    resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
+
   typescript@6.0.2:
     resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==}
     engines: {node: '>=14.17'}
@@ -3094,8 +3022,6 @@ packages:
 
 snapshots:
 
-  '@alloc/quick-lru@5.2.0': {}
-
   '@apollo/client@4.1.6(graphql-ws@6.0.8(graphql@16.13.2)(ws@8.20.0))(graphql@16.13.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)':
     dependencies:
       '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.2)
@@ -4303,10 +4229,78 @@ snapshots:
 
   '@sindresorhus/is@4.6.0': {}
 
-  '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(yaml@2.8.3))':
+  '@tailwindcss/node@4.2.2':
+    dependencies:
+      '@jridgewell/remapping': 2.3.5
+      enhanced-resolve: 5.20.1
+      jiti: 2.6.1
+      lightningcss: 1.32.0
+      magic-string: 0.30.21
+      source-map-js: 1.2.1
+      tailwindcss: 4.2.2
+
+  '@tailwindcss/oxide-android-arm64@4.2.2':
+    optional: true
+
+  '@tailwindcss/oxide-darwin-arm64@4.2.2':
+    optional: true
+
+  '@tailwindcss/oxide-darwin-x64@4.2.2':
+    optional: true
+
+  '@tailwindcss/oxide-freebsd-x64@4.2.2':
+    optional: true
+
+  '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
+    optional: true
+
+  '@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
+    optional: true
+
+  '@tailwindcss/oxide-linux-arm64-musl@4.2.2':
+    optional: true
+
+  '@tailwindcss/oxide-linux-x64-gnu@4.2.2':
+    optional: true
+
+  '@tailwindcss/oxide-linux-x64-musl@4.2.2':
+    optional: true
+
+  '@tailwindcss/oxide-wasm32-wasi@4.2.2':
+    optional: true
+
+  '@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
+    optional: true
+
+  '@tailwindcss/oxide-win32-x64-msvc@4.2.2':
+    optional: true
+
+  '@tailwindcss/oxide@4.2.2':
+    optionalDependencies:
+      '@tailwindcss/oxide-android-arm64': 4.2.2
+      '@tailwindcss/oxide-darwin-arm64': 4.2.2
+      '@tailwindcss/oxide-darwin-x64': 4.2.2
+      '@tailwindcss/oxide-freebsd-x64': 4.2.2
+      '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2
+      '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2
+      '@tailwindcss/oxide-linux-arm64-musl': 4.2.2
+      '@tailwindcss/oxide-linux-x64-gnu': 4.2.2
+      '@tailwindcss/oxide-linux-x64-musl': 4.2.2
+      '@tailwindcss/oxide-wasm32-wasi': 4.2.2
+      '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
+      '@tailwindcss/oxide-win32-x64-msvc': 4.2.2
+
+  '@tailwindcss/typography@0.5.19(tailwindcss@4.2.2)':
     dependencies:
       postcss-selector-parser: 6.0.10
-      tailwindcss: 3.4.19(yaml@2.8.3)
+      tailwindcss: 4.2.2
+
+  '@tailwindcss/vite@4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.3))':
+    dependencies:
+      '@tailwindcss/node': 4.2.2
+      '@tailwindcss/oxide': 4.2.2
+      tailwindcss: 4.2.2
+      vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.3)
 
   '@tybys/wasm-util@0.10.1':
     dependencies:
@@ -4355,10 +4349,10 @@ snapshots:
 
   '@ungap/structured-clone@1.3.0': {}
 
-  '@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@1.21.7)(yaml@2.8.3))':
+  '@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.3))':
     dependencies:
       '@rolldown/pluginutils': 1.0.0-rc.7
-      vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@1.21.7)(yaml@2.8.3)
+      vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.3)
 
   '@whatwg-node/disposablestack@0.0.6':
     dependencies:
@@ -4411,15 +4405,6 @@ snapshots:
 
   ansi-styles@6.2.3: {}
 
-  any-promise@1.3.0: {}
-
-  anymatch@3.1.3:
-    dependencies:
-      normalize-path: 3.0.0
-      picomatch: 2.3.2
-
-  arg@5.0.2: {}
-
   argparse@2.0.1: {}
 
   aria-hidden@1.2.6:
@@ -4430,23 +4415,12 @@ snapshots:
 
   auto-bind@4.0.0: {}
 
-  autoprefixer@10.4.27(postcss@8.5.8):
-    dependencies:
-      browserslist: 4.28.1
-      caniuse-lite: 1.0.30001781
-      fraction.js: 5.3.4
-      picocolors: 1.1.1
-      postcss: 8.5.8
-      postcss-value-parser: 4.2.0
-
   bail@2.0.2: {}
 
   balanced-match@4.0.4: {}
 
   baseline-browser-mapping@2.10.12: {}
 
-  binary-extensions@2.3.0: {}
-
   brace-expansion@5.0.5:
     dependencies:
       balanced-match: 4.0.4
@@ -4470,8 +4444,6 @@ snapshots:
       pascal-case: 3.1.2
       tslib: 2.8.1
 
-  camelcase-css@2.0.1: {}
-
   caniuse-lite@1.0.30001781: {}
 
   capital-case@1.0.4:
@@ -4527,18 +4499,6 @@ snapshots:
 
   chardet@2.1.1: {}
 
-  chokidar@3.6.0:
-    dependencies:
-      anymatch: 3.1.3
-      braces: 3.0.3
-      glob-parent: 5.1.2
-      is-binary-path: 2.1.0
-      is-glob: 4.0.3
-      normalize-path: 3.0.0
-      readdirp: 3.6.0
-    optionalDependencies:
-      fsevents: 2.3.3
-
   class-variance-authority@0.7.1:
     dependencies:
       clsx: 2.1.1
@@ -4572,8 +4532,6 @@ snapshots:
 
   comma-separated-tokens@2.0.3: {}
 
-  commander@4.1.1: {}
-
   common-tags@1.8.2: {}
 
   constant-case@3.0.4:
@@ -4642,14 +4600,10 @@ snapshots:
     dependencies:
       dequal: 2.0.3
 
-  didyoumean@1.2.2: {}
-
   dir-glob@3.0.1:
     dependencies:
       path-type: 4.0.0
 
-  dlv@1.1.3: {}
-
   dot-case@3.0.4:
     dependencies:
       no-case: 3.0.4
@@ -4665,6 +4619,11 @@ snapshots:
 
   emoticon@4.1.0: {}
 
+  enhanced-resolve@5.20.1:
+    dependencies:
+      graceful-fs: 4.2.11
+      tapable: 2.3.2
+
   entities@6.0.1: {}
 
   env-paths@2.2.1: {}
@@ -4714,13 +4673,9 @@ snapshots:
     dependencies:
       fetch-blob: 3.2.0
 
-  fraction.js@5.3.4: {}
-
   fsevents@2.3.3:
     optional: true
 
-  function-bind@1.1.2: {}
-
   gensync@1.0.0-beta.2: {}
 
   get-caller-file@2.0.5: {}
@@ -4735,10 +4690,6 @@ snapshots:
     dependencies:
       is-glob: 4.0.3
 
-  glob-parent@6.0.2:
-    dependencies:
-      is-glob: 4.0.3
-
   globby@11.1.0:
     dependencies:
       array-union: 2.1.0
@@ -4748,6 +4699,8 @@ snapshots:
       merge2: 1.4.1
       slash: 3.0.0
 
+  graceful-fs@4.2.11: {}
+
   graphql-config@5.1.6(@types/node@25.5.0)(graphql@16.13.2)(typescript@6.0.2):
     dependencies:
       '@graphql-tools/graphql-file-loader': 8.1.12(graphql@16.13.2)
@@ -4785,10 +4738,6 @@ snapshots:
 
   has-flag@4.0.0: {}
 
-  hasown@2.0.2:
-    dependencies:
-      function-bind: 1.1.2
-
   hast-util-from-parse5@8.0.3:
     dependencies:
       '@types/hast': 3.0.4
@@ -4928,14 +4877,6 @@ snapshots:
 
   is-arrayish@0.2.1: {}
 
-  is-binary-path@2.1.0:
-    dependencies:
-      binary-extensions: 2.3.0
-
-  is-core-module@2.16.1:
-    dependencies:
-      hasown: 2.0.2
-
   is-decimal@2.0.1: {}
 
   is-extglob@2.1.1: {}
@@ -4984,8 +4925,6 @@ snapshots:
     dependencies:
       ws: 8.20.0
 
-  jiti@1.21.7: {}
-
   jiti@2.6.1: {}
 
   js-tokens@4.0.0: {}
@@ -5054,8 +4993,6 @@ snapshots:
       lightningcss-win32-arm64-msvc: 1.32.0
       lightningcss-win32-x64-msvc: 1.32.0
 
-  lilconfig@3.1.3: {}
-
   lines-and-columns@1.2.4: {}
 
   listr2@9.0.5:
@@ -5106,6 +5043,10 @@ snapshots:
     dependencies:
       react: 19.2.4
 
+  magic-string@0.30.21:
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.5.5
+
   map-cache@0.2.2: {}
 
   markdown-table@3.0.4: {}
@@ -5475,12 +5416,6 @@ snapshots:
 
   mute-stream@2.0.0: {}
 
-  mz@2.7.0:
-    dependencies:
-      any-promise: 1.3.0
-      object-assign: 4.1.1
-      thenify-all: 1.6.0
-
   nanoid@3.3.11: {}
 
   no-case@3.0.4:
@@ -5509,12 +5444,6 @@ snapshots:
     dependencies:
       remove-trailing-separator: 1.1.0
 
-  normalize-path@3.0.0: {}
-
-  object-assign@4.1.1: {}
-
-  object-hash@3.0.0: {}
-
   onetime@7.0.0:
     dependencies:
       mimic-function: 5.0.1
@@ -5632,8 +5561,6 @@ snapshots:
       dot-case: 3.0.4
       tslib: 2.8.1
 
-  path-parse@1.0.7: {}
-
   path-root-regex@0.1.2: {}
 
   path-root@0.1.1:
@@ -5648,47 +5575,11 @@ snapshots:
 
   picomatch@4.0.4: {}
 
-  pify@2.3.0: {}
-
-  pirates@4.0.7: {}
-
-  postcss-import@15.1.0(postcss@8.5.8):
-    dependencies:
-      postcss: 8.5.8
-      postcss-value-parser: 4.2.0
-      read-cache: 1.0.0
-      resolve: 1.22.11
-
-  postcss-js@4.1.0(postcss@8.5.8):
-    dependencies:
-      camelcase-css: 2.0.1
-      postcss: 8.5.8
-
-  postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8)(yaml@2.8.3):
-    dependencies:
-      lilconfig: 3.1.3
-    optionalDependencies:
-      jiti: 1.21.7
-      postcss: 8.5.8
-      yaml: 2.8.3
-
-  postcss-nested@6.2.0(postcss@8.5.8):
-    dependencies:
-      postcss: 8.5.8
-      postcss-selector-parser: 6.1.2
-
   postcss-selector-parser@6.0.10:
     dependencies:
       cssesc: 3.0.0
       util-deprecate: 1.0.2
 
-  postcss-selector-parser@6.1.2:
-    dependencies:
-      cssesc: 3.0.0
-      util-deprecate: 1.0.2
-
-  postcss-value-parser@4.2.0: {}
-
   postcss@8.5.8:
     dependencies:
       nanoid: 3.3.11
@@ -5759,14 +5650,6 @@ snapshots:
 
   react@19.2.4: {}
 
-  read-cache@1.0.0:
-    dependencies:
-      pify: 2.3.0
-
-  readdirp@3.6.0:
-    dependencies:
-      picomatch: 2.3.2
-
   rehype-autolink-headings@7.1.0:
     dependencies:
       '@types/hast': 3.0.4
@@ -5858,12 +5741,6 @@ snapshots:
 
   resolve-from@5.0.0: {}
 
-  resolve@1.22.11:
-    dependencies:
-      is-core-module: 2.16.1
-      path-parse: 1.0.7
-      supports-preserve-symlinks-flag: 1.0.0
-
   restore-cursor@5.1.0:
     dependencies:
       onetime: 7.0.0
@@ -5992,22 +5869,10 @@ snapshots:
     dependencies:
       inline-style-parser: 0.2.7
 
-  sucrase@3.35.1:
-    dependencies:
-      '@jridgewell/gen-mapping': 0.3.13
-      commander: 4.1.1
-      lines-and-columns: 1.2.4
-      mz: 2.7.0
-      pirates: 4.0.7
-      tinyglobby: 0.2.15
-      ts-interface-checker: 0.1.13
-
   supports-color@7.2.0:
     dependencies:
       has-flag: 4.0.0
 
-  supports-preserve-symlinks-flag@1.0.0: {}
-
   swap-case@2.0.2:
     dependencies:
       tslib: 2.8.1
@@ -6020,45 +5885,9 @@ snapshots:
 
   tailwind-merge@3.5.0: {}
 
-  tailwindcss-animate@1.0.7(tailwindcss@3.4.19(yaml@2.8.3)):
-    dependencies:
-      tailwindcss: 3.4.19(yaml@2.8.3)
-
-  tailwindcss@3.4.19(yaml@2.8.3):
-    dependencies:
-      '@alloc/quick-lru': 5.2.0
-      arg: 5.0.2
-      chokidar: 3.6.0
-      didyoumean: 1.2.2
-      dlv: 1.1.3
-      fast-glob: 3.3.3
-      glob-parent: 6.0.2
-      is-glob: 4.0.3
-      jiti: 1.21.7
-      lilconfig: 3.1.3
-      micromatch: 4.0.8
-      normalize-path: 3.0.0
-      object-hash: 3.0.0
-      picocolors: 1.1.1
-      postcss: 8.5.8
-      postcss-import: 15.1.0(postcss@8.5.8)
-      postcss-js: 4.1.0(postcss@8.5.8)
-      postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8)(yaml@2.8.3)
-      postcss-nested: 6.2.0(postcss@8.5.8)
-      postcss-selector-parser: 6.1.2
-      resolve: 1.22.11
-      sucrase: 3.35.1
-    transitivePeerDependencies:
-      - tsx
-      - yaml
-
-  thenify-all@1.6.0:
-    dependencies:
-      thenify: 3.3.1
+  tailwindcss@4.2.2: {}
 
-  thenify@3.3.1:
-    dependencies:
-      any-promise: 1.3.0
+  tapable@2.3.2: {}
 
   timeout-signal@2.0.0: {}
 

webui2/src/components/bugs/BugRow.tsx πŸ”—

@@ -40,7 +40,7 @@ export function BugRow({
   const authorHref = repo ? `/${repo}/user/${author.humanId}` : `/user/${author.humanId}`;
 
   return (
-    <div className="flex items-start gap-3 border-b border-border px-4 py-3 last:border-0 hover:bg-muted/30">
+    <div className="border-border hover:bg-muted/30 flex items-start gap-3 border-b px-4 py-3 last:border-0">
       <StatusIcon
         className={
           isOpen
@@ -53,7 +53,7 @@ export function BugRow({
         <div className="flex flex-wrap items-baseline gap-2">
           <Link
             to={issueHref}
-            className="font-medium text-foreground hover:text-primary hover:underline"
+            className="text-foreground hover:text-primary font-medium hover:underline"
           >
             {title}
           </Link>
@@ -66,7 +66,7 @@ export function BugRow({
             />
           ))}
         </div>
-        <p className="mt-0.5 text-xs text-muted-foreground">
+        <p className="text-muted-foreground mt-0.5 text-xs">
           #{humanId} opened {formatDistanceToNow(new Date(createdAt), { addSuffix: true })} by{" "}
           <Link to={authorHref} className="hover:underline">
             {author.displayName}
@@ -75,7 +75,7 @@ export function BugRow({
       </div>
 
       {commentCount > 0 && (
-        <div className="flex shrink-0 items-center gap-1 text-xs text-muted-foreground">
+        <div className="text-muted-foreground flex shrink-0 items-center gap-1 text-xs">
           <MessageSquare className="size-3.5" />
           {commentCount}
         </div>

webui2/src/components/bugs/CommentBox.tsx πŸ”—

@@ -80,14 +80,14 @@ export function CommentBox({ bugPrefix, bugStatus, ref_ }: CommentBoxProps) {
         </AvatarFallback>
       </Avatar>
 
-      <div className="min-w-0 flex-1 rounded-md border border-border">
+      <div className="border-border min-w-0 flex-1 rounded-md border">
         {/* Write / Preview tabs */}
-        <div className="flex border-b border-border">
+        <div className="border-border flex border-b">
           <button
             onClick={() => setPreview(false)}
             className={`px-4 py-2 text-sm font-medium transition-colors ${
               !preview
-                ? "border-b-2 border-primary text-foreground"
+                ? "border-primary text-foreground border-b-2"
                 : "text-muted-foreground hover:text-foreground"
             }`}
           >
@@ -98,7 +98,7 @@ export function CommentBox({ bugPrefix, bugStatus, ref_ }: CommentBoxProps) {
             disabled={!hasMessage}
             className={`px-4 py-2 text-sm font-medium transition-colors disabled:opacity-40 ${
               preview
-                ? "border-b-2 border-primary text-foreground"
+                ? "border-primary text-foreground border-b-2"
                 : "text-muted-foreground hover:text-foreground"
             }`}
           >
@@ -120,7 +120,7 @@ export function CommentBox({ bugPrefix, bugStatus, ref_ }: CommentBoxProps) {
           />
         )}
 
-        <div className="flex items-center justify-end gap-2 border-t border-border px-3 py-2">
+        <div className="border-border flex items-center justify-end gap-2 border-t px-3 py-2">
           <Button
             variant="outline"
             size="sm"

webui2/src/components/bugs/IssueFilters.tsx πŸ”—

@@ -190,28 +190,28 @@ export function IssueFilters({
             <Tag className="size-3.5" />
             Labels
             {selectedLabels.length > 0 && (
-              <span className="rounded-full bg-muted px-1.5 py-0.5 text-xs leading-none">
+              <span className="bg-muted rounded-full px-1.5 py-0.5 text-xs leading-none">
                 {selectedLabels.length}
               </span>
             )}
             <ChevronDown className="size-3" />
           </button>
         </PopoverTrigger>
-        <PopoverContent align="end" className="w-56 bg-popover p-0 shadow-lg">
+        <PopoverContent align="end" className="bg-popover w-56 p-0 shadow-lg">
           {/* Search */}
-          <div className="flex items-center gap-2 border-b border-border px-3 py-2">
-            <Search className="size-3.5 shrink-0 text-muted-foreground" />
+          <div className="border-border flex items-center gap-2 border-b px-3 py-2">
+            <Search className="text-muted-foreground size-3.5 shrink-0" />
             <input
               autoFocus
               placeholder="Search labels…"
               value={labelSearch}
               onChange={(e) => setLabelSearch(e.target.value)}
-              className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
+              className="placeholder:text-muted-foreground w-full bg-transparent text-sm outline-hidden"
             />
           </div>
           <div className="max-h-64 overflow-y-auto p-1">
             {sortedLabels.length === 0 && (
-              <p className="px-2 py-3 text-center text-xs text-muted-foreground">No labels found</p>
+              <p className="text-muted-foreground px-2 py-3 text-center text-xs">No labels found</p>
             )}
             {sortedLabels.map((label) => {
               const active = selectedLabels.includes(label.name);
@@ -219,7 +219,7 @@ export function IssueFilters({
                 <button
                   key={label.name}
                   onClick={() => toggleLabel(label.name)}
-                  className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted"
+                  className="hover:bg-muted flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm"
                 >
                   <span
                     className="size-2 shrink-0 rounded-full"
@@ -229,16 +229,16 @@ export function IssueFilters({
                     }}
                   />
                   <LabelBadge name={label.name} color={label.color} />
-                  {active && <Check className="ml-auto size-3.5 shrink-0 text-foreground" />}
+                  {active && <Check className="text-foreground ml-auto size-3.5 shrink-0" />}
                 </button>
               );
             })}
           </div>
           {selectedLabels.length > 0 && (
-            <div className="border-t border-border p-1">
+            <div className="border-border border-t p-1">
               <button
                 onClick={() => onLabelsChange([])}
-                className="flex w-full items-center gap-1.5 rounded px-2 py-1.5 text-xs text-muted-foreground hover:bg-muted"
+                className="text-muted-foreground hover:bg-muted flex w-full items-center gap-1.5 rounded-sm px-2 py-1.5 text-xs"
               >
                 <X className="size-3" />
                 Clear labels
@@ -285,21 +285,21 @@ export function IssueFilters({
             <ChevronDown className="size-3" />
           </button>
         </PopoverTrigger>
-        <PopoverContent align="end" className="w-56 bg-popover p-0 shadow-lg">
+        <PopoverContent align="end" className="bg-popover w-56 p-0 shadow-lg">
           {/* Search */}
-          <div className="flex items-center gap-2 border-b border-border px-3 py-2">
-            <Search className="size-3.5 shrink-0 text-muted-foreground" />
+          <div className="border-border flex items-center gap-2 border-b px-3 py-2">
+            <Search className="text-muted-foreground size-3.5 shrink-0" />
             <input
               autoFocus
               placeholder="Search authors…"
               value={authorSearch}
               onChange={(e) => setAuthorSearch(e.target.value)}
-              className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
+              className="placeholder:text-muted-foreground w-full bg-transparent text-sm outline-hidden"
             />
           </div>
           <div className="max-h-64 overflow-y-auto p-1">
             {visibleIdentities.length === 0 && (
-              <p className="px-2 py-3 text-center text-xs text-muted-foreground">
+              <p className="text-muted-foreground px-2 py-3 text-center text-xs">
                 No authors found
               </p>
             )}
@@ -314,7 +314,7 @@ export function IssueFilters({
                       active ? null : authorQueryValue(identity),
                     )
                   }
-                  className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted"
+                  className="hover:bg-muted flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm"
                 >
                   <Avatar className="size-5 shrink-0">
                     <AvatarImage src={identity.avatarUrl ?? undefined} alt={identity.displayName} />
@@ -325,26 +325,26 @@ export function IssueFilters({
                   <div className="min-w-0 flex-1 text-left">
                     <div className="truncate">{identity.displayName}</div>
                     {identity.login && identity.login !== identity.displayName && (
-                      <div className="truncate text-xs text-muted-foreground">
+                      <div className="text-muted-foreground truncate text-xs">
                         @{identity.login}
                       </div>
                     )}
                   </div>
-                  {active && <Check className="size-3.5 shrink-0 text-foreground" />}
+                  {active && <Check className="text-foreground size-3.5 shrink-0" />}
                 </button>
               );
             })}
             {!isSearching && allIdentities.length > INITIAL_AUTHOR_LIMIT && (
-              <p className="px-2 py-1.5 text-center text-xs text-muted-foreground">
+              <p className="text-muted-foreground px-2 py-1.5 text-center text-xs">
                 {allIdentities.length - visibleIdentities.length} more β€” type to search
               </p>
             )}
           </div>
           {selectedAuthorId && (
-            <div className="border-t border-border p-1">
+            <div className="border-border border-t p-1">
               <button
                 onClick={() => onAuthorChange(null, null)}
-                className="flex w-full items-center gap-1.5 rounded px-2 py-1.5 text-xs text-muted-foreground hover:bg-muted"
+                className="text-muted-foreground hover:bg-muted flex w-full items-center gap-1.5 rounded-sm px-2 py-1.5 text-xs"
               >
                 <X className="size-3" />
                 Clear author
@@ -370,16 +370,16 @@ export function IssueFilters({
             <ChevronDown className="size-3" />
           </button>
         </PopoverTrigger>
-        <PopoverContent align="end" className="w-56 bg-popover p-1 shadow-lg">
+        <PopoverContent align="end" className="bg-popover w-56 p-1 shadow-lg">
           {SORT_OPTIONS.map((opt) => (
             <button
               key={opt.value}
               onClick={() => onSortChange(opt.value)}
-              className="flex w-full items-center gap-2 whitespace-nowrap rounded px-2 py-1.5 text-sm hover:bg-muted"
+              className="hover:bg-muted flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm whitespace-nowrap"
             >
               {opt.label}
               {sort === opt.value && (
-                <Check className="ml-auto size-3.5 shrink-0 text-foreground" />
+                <Check className="text-foreground ml-auto size-3.5 shrink-0" />
               )}
             </button>
           ))}

webui2/src/components/bugs/LabelEditor.tsx πŸ”—

@@ -46,7 +46,7 @@ export function LabelEditor({ bugPrefix, currentLabels, ref_ }: LabelEditorProps
   return (
     <div>
       <div className="mb-2 flex items-center justify-between">
-        <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
+        <h3 className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
           Labels
         </h3>
         {user && validLabels.length > 0 && (
@@ -57,7 +57,7 @@ export function LabelEditor({ bugPrefix, currentLabels, ref_ }: LabelEditorProps
               </button>
             </PopoverTrigger>
             <PopoverContent align="end" className="w-56 p-2">
-              <p className="mb-2 px-2 text-xs font-medium text-muted-foreground">Apply labels</p>
+              <p className="text-muted-foreground mb-2 px-2 text-xs font-medium">Apply labels</p>
               <div className="space-y-1">
                 {validLabels.map((label) => {
                   const active = currentNames.has(label.name);
@@ -67,7 +67,7 @@ export function LabelEditor({ bugPrefix, currentLabels, ref_ }: LabelEditorProps
                       onClick={() => {
                         void toggleLabel(label.name);
                       }}
-                      className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted"
+                      className="hover:bg-muted flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm"
                     >
                       <span
                         className={`size-2 rounded-full border-2 transition-colors ${
@@ -94,7 +94,7 @@ export function LabelEditor({ bugPrefix, currentLabels, ref_ }: LabelEditorProps
       </div>
 
       {currentLabels.length === 0 ? (
-        <p className="text-sm text-muted-foreground">None yet</p>
+        <p className="text-muted-foreground text-sm">None yet</p>
       ) : (
         <div className="flex flex-wrap gap-1">
           {currentLabels.map((label) => (

webui2/src/components/bugs/QueryInput.tsx πŸ”—

@@ -299,13 +299,13 @@ export function QueryInput({ value, onChange, onSubmit, placeholder, className }
       )}
       onClick={() => inputRef.current?.focus()}
     >
-      <Search className="pointer-events-none absolute left-3 size-4 shrink-0 text-muted-foreground" />
+      <Search className="text-muted-foreground pointer-events-none absolute left-3 size-4 shrink-0" />
 
       {/* Colored backdrop β€” same font/size/padding as the input. aria-hidden so
           screen readers only see the real input, not the duplicate text. */}
       <div
         aria-hidden
-        className="pointer-events-none absolute inset-0 flex items-center overflow-hidden whitespace-pre pl-9 pr-3 font-mono text-sm text-foreground"
+        className="text-foreground pointer-events-none absolute inset-0 flex items-center overflow-hidden pr-3 pl-9 font-mono text-sm whitespace-pre"
       >
         {value === "" ? null : segments.map((seg, i) => renderSegment(seg, i))}
       </div>
@@ -320,7 +320,7 @@ export function QueryInput({ value, onChange, onSubmit, placeholder, className }
         onChange={handleChange}
         onKeyDown={handleKeyDown}
         onSelect={handleSelect}
-        className="relative w-full bg-transparent py-2 pl-9 pr-3 font-mono text-sm text-transparent caret-foreground outline-none placeholder:font-sans placeholder:text-muted-foreground"
+        className="caret-foreground placeholder:text-muted-foreground relative w-full bg-transparent py-2 pr-3 pl-9 font-mono text-sm text-transparent outline-hidden placeholder:font-sans"
         spellCheck={false}
         autoComplete="off"
       />
@@ -329,7 +329,7 @@ export function QueryInput({ value, onChange, onSubmit, placeholder, className }
           Uses onMouseDown+preventDefault so clicking a suggestion doesn't blur
           the input before the click registers (classic focus-race problem). */}
       {showDropdown && (
-        <div className="absolute left-0 right-0 top-full z-50 mt-1 overflow-hidden rounded-md border border-border bg-popover shadow-md">
+        <div className="border-border bg-popover absolute top-full right-0 left-0 z-50 mt-1 overflow-hidden rounded-md border shadow-md">
           {suggestions.map((s, i) => (
             <button
               key={s.completedToken}
@@ -350,7 +350,7 @@ export function QueryInput({ value, onChange, onSubmit, placeholder, className }
               )}
               <span className="font-mono">{s.completedToken}</span>
               {s.display !== s.completedToken.split(":")[1]?.replace(/"/g, "") && (
-                <span className="ml-auto text-xs text-muted-foreground">{s.display}</span>
+                <span className="text-muted-foreground ml-auto text-xs">{s.display}</span>
               )}
             </button>
           ))}

webui2/src/components/bugs/Timeline.tsx πŸ”—

@@ -95,22 +95,22 @@ function CommentItem({ item, bugPrefix }: { item: CommentItem; bugPrefix: string
         </AvatarFallback>
       </Avatar>
 
-      <div className="min-w-0 flex-1 rounded-md border border-border">
-        <div className="flex items-center gap-2 border-b border-border bg-muted/40 px-4 py-2 text-sm">
+      <div className="border-border min-w-0 flex-1 rounded-md border">
+        <div className="border-border bg-muted/40 flex items-center gap-2 border-b px-4 py-2 text-sm">
           <Link
             to={repo ? `/${repo}/user/${item.author.humanId}` : `/user/${item.author.humanId}`}
-            className="font-medium text-foreground hover:underline"
+            className="text-foreground font-medium hover:underline"
           >
             {item.author.displayName}
           </Link>
           <span className="text-muted-foreground">
             {formatDistanceToNow(new Date(item.createdAt), { addSuffix: true })}
           </span>
-          {item.edited && !editing && <span className="text-xs text-muted-foreground">edited</span>}
+          {item.edited && !editing && <span className="text-muted-foreground text-xs">edited</span>}
           {canEdit && !editing && (
             <button
               onClick={() => setEditing(true)}
-              className="ml-auto rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground"
+              className="text-muted-foreground hover:bg-muted hover:text-foreground ml-auto rounded-sm px-1.5 py-0.5 text-xs"
             >
               Edit
             </button>
@@ -147,7 +147,7 @@ function CommentItem({ item, bugPrefix }: { item: CommentItem; bugPrefix: string
             {item.message ? (
               <Markdown content={item.message} />
             ) : (
-              <p className="text-sm italic text-muted-foreground">No description provided.</p>
+              <p className="text-muted-foreground text-sm italic">No description provided.</p>
             )}
           </div>
         )}
@@ -164,7 +164,7 @@ type TitleChangeItem = Extract<TimelineNode, { __typename: "BugSetTitleTimelineI
 
 function EventRow({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) {
   return (
-    <div className="flex items-center gap-3 pl-2 text-sm text-muted-foreground">
+    <div className="text-muted-foreground flex items-center gap-3 pl-2 text-sm">
       <span className="flex size-8 shrink-0 items-center justify-center">{icon}</span>
       {children}
     </div>
@@ -178,7 +178,7 @@ function LabelChangeItem({ item }: { item: LabelChangeItem }) {
       <span>
         <Link
           to={repo ? `/${repo}/user/${item.author.humanId}` : `/user/${item.author.humanId}`}
-          className="font-medium text-foreground hover:underline"
+          className="text-foreground font-medium hover:underline"
         >
           {item.author.displayName}
         </Link>{" "}
@@ -220,7 +220,7 @@ function StatusChangeItem({ item }: { item: StatusChangeItem }) {
       <span>
         <Link
           to={repo ? `/${repo}/user/${item.author.humanId}` : `/user/${item.author.humanId}`}
-          className="font-medium text-foreground hover:underline"
+          className="text-foreground font-medium hover:underline"
         >
           {item.author.displayName}
         </Link>{" "}
@@ -238,12 +238,12 @@ function TitleChangeItem({ item }: { item: TitleChangeItem }) {
       <span>
         <Link
           to={repo ? `/${repo}/user/${item.author.humanId}` : `/user/${item.author.humanId}`}
-          className="font-medium text-foreground hover:underline"
+          className="text-foreground font-medium hover:underline"
         >
           {item.author.displayName}
         </Link>{" "}
         changed the title from <span className="line-through">{item.was}</span> to{" "}
-        <span className="font-medium text-foreground">{item.title}</span>{" "}
+        <span className="text-foreground font-medium">{item.title}</span>{" "}
         {formatDistanceToNow(new Date(item.date), { addSuffix: true })}
       </span>
     </EventRow>

webui2/src/components/bugs/TitleEditor.tsx πŸ”—

@@ -87,14 +87,14 @@ export function TitleEditor({ bugPrefix, title, humanId, ref_ }: TitleEditorProp
 
   return (
     <div className="group flex items-start gap-2">
-      <h1 className="flex-1 text-2xl font-semibold leading-tight text-foreground">
+      <h1 className="text-foreground flex-1 text-2xl leading-tight font-semibold">
         {title}
-        <span className="ml-2 text-xl font-normal text-muted-foreground">#{humanId}</span>
+        <span className="text-muted-foreground ml-2 text-xl font-normal">#{humanId}</span>
       </h1>
       {user && (
         <button
           onClick={() => setEditing(true)}
-          className="mt-1 shrink-0 text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100"
+          className="text-muted-foreground hover:text-foreground mt-1 shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
           title="Edit title"
         >
           <Pencil className="size-4" />

webui2/src/components/code/CodeBreadcrumb.tsx πŸ”—

@@ -17,7 +17,7 @@ export function CodeBreadcrumb({ repoName, ref, path, onNavigate }: CodeBreadcru
     <div className="flex flex-wrap items-center gap-1 font-mono text-sm">
       <button
         onClick={() => onNavigate("")}
-        className="font-medium text-foreground hover:underline"
+        className="text-foreground font-medium hover:underline"
       >
         {repoName}
       </button>
@@ -27,9 +27,9 @@ export function CodeBreadcrumb({ repoName, ref, path, onNavigate }: CodeBreadcru
         const isLast = i === parts.length - 1;
         return (
           <span key={partPath} className="flex items-center gap-1">
-            <ChevronRight className="size-3.5 text-muted-foreground" />
+            <ChevronRight className="text-muted-foreground size-3.5" />
             {isLast ? (
-              <span className="font-medium text-foreground">{part}</span>
+              <span className="text-foreground font-medium">{part}</span>
             ) : (
               <button
                 onClick={() => onNavigate(partPath)}
@@ -42,7 +42,7 @@ export function CodeBreadcrumb({ repoName, ref, path, onNavigate }: CodeBreadcru
         );
       })}
 
-      <span className="ml-2 text-xs text-muted-foreground">@ {ref}</span>
+      <span className="text-muted-foreground ml-2 text-xs">@ {ref}</span>
     </div>
   );
 }

webui2/src/components/code/CommitList.tsx πŸ”—

@@ -5,7 +5,7 @@ import { gql } from "@apollo/client";
 import { useQuery } from "@apollo/client/react";
 import { formatDistanceToNow } from "date-fns";
 import { GitCommit } from "lucide-react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
 import { Link } from "react-router";
 
 import { Button } from "@/components/ui/button";
@@ -34,6 +34,15 @@ const COMMITS_QUERY = gql`
 
 const PAGE_SIZE = 30;
 
+interface CommitListQueryData {
+  repository: {
+    commits: {
+      nodes: CommitNode[];
+      pageInfo: { hasNextPage: boolean; endCursor: string | null };
+    } | null;
+  } | null;
+}
+
 interface CommitListProps {
   ref_: string;
   path?: string;
@@ -52,16 +61,17 @@ export function CommitList({ ref_, path }: CommitListProps) {
   const [cursor, setCursor] = useState<string | null>(null);
   const [allCommits, setAllCommits] = useState<CommitNode[]>([]);
 
-  const { loading, error, fetchMore } = useQuery(COMMITS_QUERY, {
+  const { data, loading, error, fetchMore } = useQuery<CommitListQueryData>(COMMITS_QUERY, {
     variables: { repo, ref: ref_, path: path ?? null, after: null, first: PAGE_SIZE },
     skip: !ref_,
-    onCompleted(data) {
-      const nodes = data?.repository?.commits?.nodes ?? [];
-      setAllCommits(nodes);
-      setCursor(data?.repository?.commits?.pageInfo?.endCursor ?? null);
-    },
   });
 
+  useEffect(() => {
+    const nodes = data?.repository?.commits?.nodes ?? [];
+    setAllCommits(nodes);
+    setCursor(data?.repository?.commits?.pageInfo?.endCursor ?? null);
+  }, [data]);
+
   const hasMore = !!cursor && allCommits.length > 0 && allCommits.length % PAGE_SIZE === 0;
   const [loadingMore, setLoadingMore] = useState(false);
 
@@ -83,7 +93,7 @@ export function CommitList({ ref_, path }: CommitListProps) {
 
   if (error) {
     return (
-      <div className="rounded-md border border-border px-4 py-8 text-center text-sm text-destructive">
+      <div className="border-border text-destructive rounded-md border px-4 py-8 text-center text-sm">
         {error.message}
       </div>
     );
@@ -95,10 +105,10 @@ export function CommitList({ ref_, path }: CommitListProps) {
     <div className="space-y-6">
       {groups.map(([date, group]) => (
         <div key={date}>
-          <h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
+          <h3 className="text-muted-foreground mb-2 text-xs font-semibold tracking-wider uppercase">
             Commits on {date}
           </h3>
-          <div className="divide-y divide-border overflow-hidden rounded-md border border-border">
+          <div className="divide-border border-border divide-y overflow-hidden rounded-md border">
             {group.map((commit) => (
               <CommitRow key={commit.hash} commit={commit} repo={repo} />
             ))}
@@ -120,23 +130,23 @@ export function CommitList({ ref_, path }: CommitListProps) {
 function CommitRow({ commit, repo }: { commit: CommitNode; repo: string | null }) {
   const commitPath = repo ? `/${repo}/commit/${commit.hash}` : `/commit/${commit.hash}`;
   return (
-    <div className="flex items-center gap-3 bg-background px-4 py-3 hover:bg-muted/30">
-      <GitCommit className="size-4 shrink-0 text-muted-foreground" />
+    <div className="bg-background hover:bg-muted/30 flex items-center gap-3 px-4 py-3">
+      <GitCommit className="text-muted-foreground size-4 shrink-0" />
       <div className="min-w-0 flex-1">
         <Link
           to={commitPath}
-          className="block truncate font-medium text-foreground hover:text-primary hover:underline"
+          className="text-foreground hover:text-primary block truncate font-medium hover:underline"
         >
           {commit.message}
         </Link>
-        <p className="mt-0.5 text-xs text-muted-foreground">
+        <p className="text-muted-foreground mt-0.5 text-xs">
           {commit.authorName} &middot;{" "}
           {formatDistanceToNow(new Date(commit.date), { addSuffix: true })}
         </p>
       </div>
       <Link
         to={commitPath}
-        className="shrink-0 font-mono text-xs text-muted-foreground hover:text-foreground hover:underline"
+        className="text-muted-foreground hover:text-foreground shrink-0 font-mono text-xs hover:underline"
         title={commit.hash}
       >
         {commit.shortHash}
@@ -166,10 +176,10 @@ function CommitListSkeleton() {
       {Array.from({ length: 2 }).map((_, g) => (
         <div key={g}>
           <Skeleton className="mb-2 h-3 w-32" />
-          <div className="divide-y divide-border overflow-hidden rounded-md border border-border">
+          <div className="divide-border border-border divide-y overflow-hidden rounded-md border">
             {Array.from({ length: 4 }).map((_, i) => (
               <div key={i} className="flex items-center gap-3 px-4 py-3">
-                <Skeleton className="size-4 rounded" />
+                <Skeleton className="size-4 rounded-sm" />
                 <div className="flex-1 space-y-1.5">
                   <Skeleton className="h-4 w-2/3" />
                   <Skeleton className="h-3 w-1/4" />

webui2/src/components/code/FileDiffView.tsx πŸ”—

@@ -37,6 +37,21 @@ const DIFF_QUERY = gql`
   }
 `;
 
+interface DiffQueryData {
+  repository: {
+    commit: {
+      diff: {
+        path: string;
+        oldPath: string | null;
+        isBinary: boolean;
+        isNew: boolean;
+        isDelete: boolean;
+        hunks: HunkType[];
+      } | null;
+    } | null;
+  } | null;
+}
+
 interface FileDiffViewProps {
   hash: string;
   path: string;
@@ -60,7 +75,7 @@ const statusBadge: Record<string, string> = {
 export function FileDiffView({ hash, path, oldPath, status }: FileDiffViewProps) {
   const repo = useRepo();
   const [open, setOpen] = useState(false);
-  const [fetchDiff, { data, loading, error }] = useLazyQuery(DIFF_QUERY);
+  const [fetchDiff, { data, loading, error }] = useLazyQuery<DiffQueryData>(DIFF_QUERY);
 
   function toggle() {
     if (!open && !data && !loading) {
@@ -72,10 +87,10 @@ export function FileDiffView({ hash, path, oldPath, status }: FileDiffViewProps)
   const diff = data?.repository?.commit?.diff;
 
   return (
-    <div className="divide-y divide-border">
+    <div className="divide-border divide-y">
       <button
         onClick={toggle}
-        className="flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
+        className="hover:bg-muted/50 flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors"
       >
         <ChevronRight
           className={cn(
@@ -83,7 +98,7 @@ export function FileDiffView({ hash, path, oldPath, status }: FileDiffViewProps)
             open && "rotate-90",
           )}
         />
-        {statusIcon[status] ?? <FileEdit className="size-3.5 text-muted-foreground" />}
+        {statusIcon[status] ?? <FileEdit className="text-muted-foreground size-3.5" />}
         <span className="min-w-0 flex-1 font-mono text-sm">
           {status === "RENAMED" ? (
             <>
@@ -95,24 +110,24 @@ export function FileDiffView({ hash, path, oldPath, status }: FileDiffViewProps)
             path
           )}
         </span>
-        <span className="shrink-0 rounded border border-border px-1.5 py-0.5 font-mono text-xs text-muted-foreground">
+        <span className="border-border text-muted-foreground shrink-0 rounded-sm border px-1.5 py-0.5 font-mono text-xs">
           {statusBadge[status] ?? "?"}
         </span>
       </button>
 
       {open && (
         <div className="overflow-x-auto">
-          {loading && <div className="px-4 py-3 text-xs text-muted-foreground">Loading diff…</div>}
+          {loading && <div className="text-muted-foreground px-4 py-3 text-xs">Loading diff…</div>}
           {error && (
-            <div className="px-4 py-3 text-xs text-destructive">
+            <div className="text-destructive px-4 py-3 text-xs">
               Failed to load diff: {error.message}
             </div>
           )}
           {diff &&
             (diff.isBinary ? (
-              <div className="px-4 py-3 text-xs text-muted-foreground">Binary file</div>
+              <div className="text-muted-foreground px-4 py-3 text-xs">Binary file</div>
             ) : diff.hunks.length === 0 ? (
-              <div className="px-4 py-3 text-xs text-muted-foreground">No changes</div>
+              <div className="text-muted-foreground px-4 py-3 text-xs">No changes</div>
             ) : (
               diff.hunks.map((hunk: HunkType, i: number) => <Hunk key={i} hunk={hunk} />)
             ))}
@@ -134,7 +149,7 @@ type HunkType = {
 function Hunk({ hunk }: { hunk: HunkType }) {
   return (
     <div className="font-mono text-xs leading-5">
-      <div className="select-none bg-blue-50 px-4 py-0.5 text-blue-600 dark:bg-blue-950/40 dark:text-blue-400">
+      <div className="bg-blue-50 px-4 py-0.5 text-blue-600 select-none dark:bg-blue-950/40 dark:text-blue-400">
         @@ -{hunk.oldStart},{hunk.oldLines} +{hunk.newStart},{hunk.newLines} @@
       </div>
       {hunk.lines.map((line, i) => (
@@ -146,10 +161,10 @@ function Hunk({ hunk }: { hunk: HunkType }) {
             line.type === "DELETED" && "bg-red-50    dark:bg-red-950/30",
           )}
         >
-          <span className="w-10 shrink-0 select-none border-r border-border/50 px-2 text-right text-muted-foreground/50">
+          <span className="border-border/50 text-muted-foreground/50 w-10 shrink-0 border-r px-2 text-right select-none">
             {line.oldLine || ""}
           </span>
-          <span className="w-10 shrink-0 select-none border-r border-border/50 px-2 text-right text-muted-foreground/50">
+          <span className="border-border/50 text-muted-foreground/50 w-10 shrink-0 border-r px-2 text-right select-none">
             {line.newLine || ""}
           </span>
           <span

webui2/src/components/code/FileTree.tsx πŸ”—

@@ -35,17 +35,17 @@ export function FileTree({ entries, path, loading, onNavigate, onNavigateUp }: F
   if (loading) return <FileTreeSkeleton />;
 
   return (
-    <div className="overflow-hidden rounded-md border border-border">
+    <div className="border-border overflow-hidden rounded-md border">
       <table className="w-full text-sm">
-        <tbody className="divide-y divide-border">
+        <tbody className="divide-border divide-y">
           {path && (
-            <tr className="cursor-pointer hover:bg-muted/40" onClick={onNavigateUp}>
+            <tr className="hover:bg-muted/40 cursor-pointer" onClick={onNavigateUp}>
               <td className="w-6 py-2 pl-4">
                 <Folder className="size-4 text-blue-500 dark:text-blue-400" />
               </td>
-              <td className="px-3 py-2 font-mono text-muted-foreground">..</td>
-              <td className="hidden px-3 py-2 text-muted-foreground md:table-cell" />
-              <td className="hidden px-4 py-2 text-right text-muted-foreground md:table-cell" />
+              <td className="text-muted-foreground px-3 py-2 font-mono">..</td>
+              <td className="text-muted-foreground hidden px-3 py-2 md:table-cell" />
+              <td className="text-muted-foreground hidden px-4 py-2 text-right md:table-cell" />
             </tr>
           )}
           {sorted.map((entry) => (
@@ -68,20 +68,20 @@ function FileTreeRow({
   const repo = useRepo();
 
   return (
-    <tr className="cursor-pointer hover:bg-muted/40" onClick={() => onNavigate(entry)}>
+    <tr className="hover:bg-muted/40 cursor-pointer" onClick={() => onNavigate(entry)}>
       <td className="w-6 py-2 pl-4">
         {isDir ? (
           <Folder className="size-4 text-blue-500 dark:text-blue-400" />
         ) : (
-          <File className="size-4 text-muted-foreground" />
+          <File className="text-muted-foreground size-4" />
         )}
       </td>
       <td className="px-3 py-2">
-        <span className={`font-mono ${isDir ? "font-medium text-foreground" : "text-foreground"}`}>
+        <span className={`font-mono ${isDir ? "text-foreground font-medium" : "text-foreground"}`}>
           {entry.name}
         </span>
       </td>
-      <td className="hidden max-w-xs truncate px-3 py-2 text-muted-foreground md:table-cell">
+      <td className="text-muted-foreground hidden max-w-xs truncate px-3 py-2 md:table-cell">
         {entry.lastCommit && (
           <Link
             to={
@@ -94,7 +94,7 @@ function FileTreeRow({
           </Link>
         )}
       </td>
-      <td className="hidden whitespace-nowrap px-4 py-2 text-right text-xs text-muted-foreground md:table-cell">
+      <td className="text-muted-foreground hidden px-4 py-2 text-right text-xs whitespace-nowrap md:table-cell">
         {entry.lastCommit &&
           formatDistanceToNow(new Date(entry.lastCommit.date), { addSuffix: true })}
       </td>
@@ -104,11 +104,11 @@ function FileTreeRow({
 
 function FileTreeSkeleton() {
   return (
-    <div className="overflow-hidden rounded-md border border-border">
-      <div className="divide-y divide-border">
+    <div className="border-border overflow-hidden rounded-md border">
+      <div className="divide-border divide-y">
         {Array.from({ length: 8 }).map((_, i) => (
           <div key={i} className="flex items-center gap-3 px-4 py-2">
-            <Skeleton className="size-4 rounded" />
+            <Skeleton className="size-4 rounded-sm" />
             <Skeleton className="h-4 w-32" />
             <Skeleton className="ml-6 hidden h-4 w-64 md:block" />
             <Skeleton className="ml-auto hidden h-4 w-20 md:block" />

webui2/src/components/code/FileViewer.tsx πŸ”—

@@ -47,8 +47,8 @@ export function FileViewer({ blob, loading }: FileViewerProps) {
   }
 
   return (
-    <div className="overflow-hidden rounded-md border border-border">
-      <div className="flex items-center justify-between border-b border-border bg-muted/40 px-4 py-2 text-xs text-muted-foreground">
+    <div className="border-border overflow-hidden rounded-md border">
+      <div className="border-border bg-muted/40 text-muted-foreground flex items-center justify-between border-b px-4 py-2 text-xs">
         <span>
           {lineCount.toLocaleString()} lines Β· {formatBytes(blob.size)}
           {blob.isTruncated && " Β· truncated"}
@@ -65,13 +65,13 @@ export function FileViewer({ blob, loading }: FileViewerProps) {
       </div>
 
       {blob.isBinary ? (
-        <div className="px-4 py-8 text-center text-sm text-muted-foreground">
+        <div className="text-muted-foreground px-4 py-8 text-center text-sm">
           Binary file β€” {formatBytes(blob.size)}
         </div>
       ) : (
         <div className="flex overflow-x-auto font-mono text-xs leading-5">
           <div
-            className="select-none border-r border-border bg-muted/20 px-4 py-4 text-right text-muted-foreground/50"
+            className="border-border bg-muted/20 text-muted-foreground/50 border-r px-4 py-4 text-right select-none"
             aria-hidden
           >
             {Array.from({ length: lineCount }, (_, i) => (
@@ -95,8 +95,8 @@ function formatBytes(bytes: number): string {
 
 function FileViewerSkeleton() {
   return (
-    <div className="overflow-hidden rounded-md border border-border">
-      <div className="border-b border-border bg-muted/40 px-4 py-2">
+    <div className="border-border overflow-hidden rounded-md border">
+      <div className="border-border bg-muted/40 border-b px-4 py-2">
         <Skeleton className="h-4 w-32" />
       </div>
       <div className="flex gap-4 p-4">

webui2/src/components/code/RefSelector.tsx πŸ”—

@@ -29,11 +29,11 @@ export function RefSelector({ refs, currentRef, onSelect }: RefSelectorProps) {
         <Button variant="outline" size="sm" className="gap-2 font-mono text-xs">
           <GitBranch className="size-3.5" />
           {currentRef}
-          <ChevronsUpDown className="size-3 text-muted-foreground" />
+          <ChevronsUpDown className="text-muted-foreground size-3" />
         </Button>
       </PopoverTrigger>
       <PopoverContent align="start" className="w-64 p-2">
-        <p className="mb-2 px-1 text-xs font-semibold text-muted-foreground">Switch branch / tag</p>
+        <p className="text-muted-foreground mb-2 px-1 text-xs font-semibold">Switch branch / tag</p>
         <Input
           placeholder="Filter…"
           className="mb-2 h-7 text-xs"
@@ -44,7 +44,7 @@ export function RefSelector({ refs, currentRef, onSelect }: RefSelectorProps) {
         <div className="max-h-64 overflow-y-auto">
           {branches.length > 0 && (
             <div className="mb-1">
-              <p className="px-2 py-1 text-xs text-muted-foreground">Branches</p>
+              <p className="text-muted-foreground px-2 py-1 text-xs">Branches</p>
               {branches.map((ref) => (
                 <RefItem
                   key={ref.name}
@@ -61,7 +61,7 @@ export function RefSelector({ refs, currentRef, onSelect }: RefSelectorProps) {
           )}
           {tags.length > 0 && (
             <div>
-              <p className="px-2 py-1 text-xs text-muted-foreground">Tags</p>
+              <p className="text-muted-foreground px-2 py-1 text-xs">Tags</p>
               {tags.map((ref) => (
                 <RefItem
                   key={ref.name}
@@ -77,7 +77,7 @@ export function RefSelector({ refs, currentRef, onSelect }: RefSelectorProps) {
             </div>
           )}
           {filtered.length === 0 && (
-            <p className="px-2 py-2 text-xs text-muted-foreground">No results</p>
+            <p className="text-muted-foreground px-2 py-2 text-xs">No results</p>
           )}
         </div>
       </PopoverContent>
@@ -98,17 +98,17 @@ function RefItem({
     <button
       onClick={onSelect}
       className={cn(
-        "flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs hover:bg-muted",
+        "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-xs hover:bg-muted",
         active && "font-medium",
       )}
     >
       {ref_.type === "BRANCH" ? (
-        <GitBranch className="size-3 shrink-0 text-muted-foreground" />
+        <GitBranch className="text-muted-foreground size-3 shrink-0" />
       ) : (
-        <Tag className="size-3 shrink-0 text-muted-foreground" />
+        <Tag className="text-muted-foreground size-3 shrink-0" />
       )}
       <span className="flex-1 truncate font-mono">{ref_.shortName}</span>
-      {active && <Check className="size-3 text-muted-foreground" />}
+      {active && <Check className="text-muted-foreground size-3" />}
     </button>
   );
 }

webui2/src/components/content/Markdown.tsx πŸ”—

@@ -34,24 +34,27 @@ interface MarkdownProps {
 // lists, strikethrough). Used in Timeline comments and NewBugPage preview.
 export function Markdown({ content, className }: MarkdownProps) {
   return (
-    <ReactMarkdown
-      remarkPlugins={[remarkGfm, remarkEmoji]}
-      rehypePlugins={[
-        rehypeRaw,
-        [rehypeSanitize, sanitizeSchema],
-        rehypeSlug,
-        [rehypeAutolinkHeadings, { behavior: "append" }],
-        [rehypeExternalLinks, { target: "_blank", rel: ["noopener", "noreferrer"] }],
-      ]}
+    <div
       className={cn(
         "prose prose-sm dark:prose-invert max-w-none",
         "prose-pre:bg-muted prose-pre:text-foreground",
-        "prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:before:content-none prose-code:after:content-none",
+        "prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded-sm prose-code:text-sm prose-code:before:content-none prose-code:after:content-none",
         "prose-img:inline prose-img:my-0",
         className,
       )}
     >
-      {content}
-    </ReactMarkdown>
+      <ReactMarkdown
+        remarkPlugins={[remarkGfm, remarkEmoji]}
+        rehypePlugins={[
+          rehypeRaw,
+          [rehypeSanitize, sanitizeSchema],
+          rehypeSlug,
+          [rehypeAutolinkHeadings, { behavior: "append" }],
+          [rehypeExternalLinks, { target: "_blank", rel: ["noopener", "noreferrer"] }],
+        ]}
+      >
+        {content}
+      </ReactMarkdown>
+    </div>
   );
 }

webui2/src/components/layout/Header.tsx πŸ”—

@@ -44,10 +44,10 @@ export function Header() {
   const effectiveRepo = repo === "auth" ? null : repo;
 
   return (
-    <header className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur">
+    <header className="border-border bg-background/95 sticky top-0 z-50 border-b backdrop-blur">
       <div className="mx-auto flex h-14 max-w-screen-xl items-center gap-6 px-4">
         {/* Logo always goes to the repo picker root */}
-        <Link to="/" className="flex items-center gap-2 font-semibold text-foreground">
+        <Link to="/" className="text-foreground flex items-center gap-2 font-semibold">
           <Bug className="size-4" />
           <span>git-bug</span>
         </Link>
@@ -86,7 +86,7 @@ export function Header() {
         )}
 
         <div className="ml-auto flex items-center gap-2">
-          {mode === "readonly" && <span className="text-xs text-muted-foreground">Read only</span>}
+          {mode === "readonly" && <span className="text-muted-foreground text-xs">Read only</span>}
 
           <Button variant="ghost" size="icon" onClick={toggle} title="Toggle theme">
             {theme === "light" ? <Moon className="size-4" /> : <Sun className="size-4" />}

webui2/src/components/layout/Shell.tsx πŸ”—

@@ -6,7 +6,7 @@ import { Header } from "./Header";
 // Header above the current route's page component via <Outlet>.
 export function Shell() {
   return (
-    <div className="min-h-screen bg-background font-sans antialiased">
+    <div className="bg-background min-h-screen font-sans antialiased">
       <Header />
       <main className="mx-auto max-w-screen-xl px-4 py-6">
         <Outlet />

webui2/src/components/ui/badge.tsx πŸ”—

@@ -4,15 +4,16 @@ import * as React from "react";
 import { cn } from "@/lib/utils";
 
 const badgeVariants = cva(
-  "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+  "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
   {
     variants: {
       variant: {
-        default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
+        default:
+          "border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary/80",
         secondary:
           "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
         destructive:
-          "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
+          "border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80",
         outline: "text-foreground",
       },
     },

webui2/src/components/ui/button.tsx πŸ”—

@@ -5,15 +5,15 @@ import * as React from "react";
 import { cn } from "@/lib/utils";
 
 const buttonVariants = cva(
-  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
   {
     variants: {
       variant: {
-        default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
-        destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+        default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
+        destructive: "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
         outline:
-          "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
-        secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+          "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
+        secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
         ghost: "hover:bg-accent hover:text-accent-foreground",
         link: "text-primary underline-offset-4 hover:underline",
       },

webui2/src/components/ui/input.tsx πŸ”—

@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
       <input
         type={type}
         className={cn(
-          "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+          "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
           className,
         )}
         ref={ref}

webui2/src/components/ui/popover.tsx πŸ”—

@@ -17,7 +17,7 @@ const PopoverContent = React.forwardRef<
       align={align}
       sideOffset={sideOffset}
       className={cn(
-        "z-50 w-72 rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+        "z-50 w-72 rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
         className,
       )}
       {...props}

webui2/src/components/ui/textarea.tsx πŸ”—

@@ -7,7 +7,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"tex
     return (
       <textarea
         className={cn(
-          "flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+          "flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
           className,
         )}
         ref={ref}

webui2/src/index.css πŸ”—

@@ -1,69 +1,119 @@
-/* highlight.js theme must be imported before any @layer rules (PostCSS requirement) */
+/* highlight.js theme must be imported before tailwind */
 @import "highlight.js/styles/github.css";
 
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
+@import "tailwindcss";
+@import "tw-animate-css";
+@plugin "@tailwindcss/typography";
 
-@layer base {
-  :root {
-    /* Blue-accented light palette. Primary is GitHub-style indigo-blue so
-       action buttons are clearly coloured, not a flat dark grey. */
-    --background: 0 0% 100%;
-    --foreground: 222 20% 18%;
-    --card: 0 0% 100%;
-    --card-foreground: 222 20% 18%;
-    --popover: 0 0% 100%;
-    --popover-foreground: 222 20% 18%;
-    --primary: 212 88% 44%;
-    --primary-foreground: 0 0% 100%;
-    --secondary: 214 32% 95%;
-    --secondary-foreground: 222 20% 18%;
-    --muted: 214 32% 96%;
-    --muted-foreground: 220 9% 46%;
-    --accent: 214 88% 95%;
-    --accent-foreground: 212 88% 35%;
-    --destructive: 0 84.2% 60.2%;
-    --destructive-foreground: 0 0% 98%;
-    --border: 214 32% 88%;
-    --input: 214 32% 88%;
-    --ring: 212 88% 44%;
-    --radius: 0.5rem;
-  }
+@custom-variant dark (&:where(.dark, .dark *));
+
+:root {
+  /* Blue-accented light palette. Primary is GitHub-style indigo-blue so
+     action buttons are clearly coloured, not a flat dark grey. */
+  --background: hsl(0 0% 100%);
+  --foreground: hsl(222 20% 18%);
+  --card: hsl(0 0% 100%);
+  --card-foreground: hsl(222 20% 18%);
+  --popover: hsl(0 0% 100%);
+  --popover-foreground: hsl(222 20% 18%);
+  --primary: hsl(212 88% 44%);
+  --primary-foreground: hsl(0 0% 100%);
+  --secondary: hsl(214 32% 95%);
+  --secondary-foreground: hsl(222 20% 18%);
+  --muted: hsl(214 32% 96%);
+  --muted-foreground: hsl(220 9% 46%);
+  --accent: hsl(214 88% 95%);
+  --accent-foreground: hsl(212 88% 35%);
+  --destructive: hsl(0 84.2% 60.2%);
+  --destructive-foreground: hsl(0 0% 98%);
+  --border: hsl(214 32% 88%);
+  --input: hsl(214 32% 88%);
+  --ring: hsl(212 88% 44%);
+  --radius: 0.5rem;
+}
+
+.dark {
+  /* Softer dark β€” background lifted slightly, text dimmed to reduce glare. */
+  --background: hsl(220 13% 15%);
+  --foreground: hsl(220 10% 72%);
+  --card: hsl(220 13% 18%);
+  --card-foreground: hsl(220 10% 72%);
+  --popover: hsl(220 13% 18%);
+  --popover-foreground: hsl(220 10% 72%);
+  --primary: hsl(213 88% 62%);
+  --primary-foreground: hsl(220 20% 10%);
+  --secondary: hsl(220 12% 24%);
+  --secondary-foreground: hsl(220 10% 72%);
+  --muted: hsl(220 12% 24%);
+  --muted-foreground: hsl(220 8% 52%);
+  --accent: hsl(220 20% 28%);
+  --accent-foreground: hsl(213 88% 72%);
+  --destructive: hsl(0 65% 50%);
+  --destructive-foreground: hsl(0 0% 98%);
+  --border: hsl(220 12% 26%);
+  --input: hsl(220 12% 26%);
+  --ring: hsl(213 88% 62%);
+}
+
+@theme inline {
+  --color-background: var(--background);
+  --color-foreground: var(--foreground);
+  --color-card: var(--card);
+  --color-card-foreground: var(--card-foreground);
+  --color-popover: var(--popover);
+  --color-popover-foreground: var(--popover-foreground);
+  --color-primary: var(--primary);
+  --color-primary-foreground: var(--primary-foreground);
+  --color-secondary: var(--secondary);
+  --color-secondary-foreground: var(--secondary-foreground);
+  --color-muted: var(--muted);
+  --color-muted-foreground: var(--muted-foreground);
+  --color-accent: var(--accent);
+  --color-accent-foreground: var(--accent-foreground);
+  --color-destructive: var(--destructive);
+  --color-destructive-foreground: var(--destructive-foreground);
+  --color-border: var(--border);
+  --color-input: var(--input);
+  --color-ring: var(--ring);
+
+  --radius-lg: var(--radius);
+  --radius-md: calc(var(--radius) - 2px);
+  --radius-sm: calc(var(--radius) - 4px);
+
+  --font-sans: ui-sans-serif, system-ui, sans-serif;
+  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
 
-  .dark {
-    /* Softer dark β€” background lifted slightly, text dimmed to reduce glare. */
-    --background: 220 13% 15%;
-    --foreground: 220 10% 72%;
-    --card: 220 13% 18%;
-    --card-foreground: 220 10% 72%;
-    --popover: 220 13% 18%;
-    --popover-foreground: 220 10% 72%;
-    --primary: 213 88% 62%;
-    --primary-foreground: 220 20% 10%;
-    --secondary: 220 12% 24%;
-    --secondary-foreground: 220 10% 72%;
-    --muted: 220 12% 24%;
-    --muted-foreground: 220 8% 52%;
-    --accent: 220 20% 28%;
-    --accent-foreground: 213 88% 72%;
-    --destructive: 0 65% 50%;
-    --destructive-foreground: 0 0% 98%;
-    --border: 220 12% 26%;
-    --input: 220 12% 26%;
-    --ring: 213 88% 62%;
+  --animate-accordion-down: accordion-down 0.2s ease-out;
+  --animate-accordion-up: accordion-up 0.2s ease-out;
+}
+
+@keyframes accordion-down {
+  from {
+    height: 0;
+  }
+  to {
+    height: var(--radix-accordion-content-height);
   }
 }
 
-@layer base {
-  * {
-    @apply border-border;
+@keyframes accordion-up {
+  from {
+    height: var(--radix-accordion-content-height);
   }
-  body {
-    @apply bg-background text-foreground;
+  to {
+    height: 0;
   }
 }
 
+* {
+  border-color: var(--border);
+}
+
+body {
+  background-color: var(--background);
+  color: var(--foreground);
+}
+
 /* ── Dark-mode overrides for highlight.js (imported above) ──────────────── */
 .dark .hljs {
   background: hsl(220, 13%, 16%);

webui2/src/lib/auth.tsx πŸ”—

@@ -76,7 +76,9 @@ function LocalAuthProvider({
   children: ReactNode;
   loginProviders: string[];
 }) {
-  const { data, loading } = useQuery(USER_IDENTITY_QUERY);
+  const { data, loading } = useQuery<{ repository: { userIdentity: AuthUser | null } }>(
+    USER_IDENTITY_QUERY,
+  );
   const user: AuthUser | null = data?.repository?.userIdentity ?? null;
   const mode: AuthMode = loading ? "local" : user ? "local" : "readonly";
   return (

webui2/src/pages/BugDetailPage.tsx πŸ”—

@@ -24,7 +24,7 @@ export function BugDetailPage() {
 
   if (error) {
     return (
-      <div className="py-16 text-center text-sm text-destructive">
+      <div className="text-destructive py-16 text-center text-sm">
         Failed to load issue: {error.message}
       </div>
     );
@@ -36,7 +36,7 @@ export function BugDetailPage() {
 
   const bug = data?.repository?.bug;
   if (!bug) {
-    return <div className="py-16 text-center text-sm text-muted-foreground">Issue not found.</div>;
+    return <div className="text-muted-foreground py-16 text-center text-sm">Issue not found.</div>;
   }
 
   const issuesHref = repo ? `/${repo}/issues` : "/issues";
@@ -46,7 +46,7 @@ export function BugDetailPage() {
     <div>
       <Link
         to={issuesHref}
-        className="mb-4 flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
+        className="text-muted-foreground hover:text-foreground mb-4 flex items-center gap-1.5 text-sm"
       >
         <ArrowLeft className="size-3.5" />
         Back to issues
@@ -57,10 +57,10 @@ export function BugDetailPage() {
         <TitleEditor bugPrefix={bug.humanId} title={bug.title} humanId={bug.humanId} ref_={repo} />
       </div>
 
-      <div className="mb-6 flex flex-wrap items-center gap-3 text-sm text-muted-foreground">
+      <div className="text-muted-foreground mb-6 flex flex-wrap items-center gap-3 text-sm">
         <StatusBadge status={bug.status} />
         <span>
-          <Link to={authorHref} className="font-medium text-foreground hover:underline">
+          <Link to={authorHref} className="text-foreground font-medium hover:underline">
             {bug.author.displayName}
           </Link>{" "}
           opened this issue {formatDistanceToNow(new Date(bug.createdAt), { addSuffix: true })}
@@ -83,7 +83,7 @@ export function BugDetailPage() {
           <Separator />
 
           <div>
-            <h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
+            <h3 className="text-muted-foreground mb-2 text-xs font-semibold tracking-wider uppercase">
               Participants
             </h3>
             <div className="flex flex-wrap gap-1.5">
@@ -117,7 +117,7 @@ function BugDetailSkeleton() {
       <div className="flex gap-8">
         <div className="flex-1 space-y-4">
           {Array.from({ length: 3 }).map((_, i) => (
-            <div key={i} className="rounded-md border border-border p-4">
+            <div key={i} className="border-border rounded-md border p-4">
               <Skeleton className="mb-3 h-4 w-1/4" />
               <Skeleton className="h-16 w-full" />
             </div>

webui2/src/pages/BugListPage.tsx πŸ”—

@@ -123,9 +123,9 @@ export function BugListPage() {
       </form>
 
       {/* List container */}
-      <div className="rounded-md border border-border">
+      <div className="border-border rounded-md border">
         {/* Open / Closed toggle + filter dropdowns */}
-        <div className="flex items-center gap-2 overflow-x-auto border-b border-border px-4 py-2">
+        <div className="border-border flex items-center gap-2 overflow-x-auto border-b px-4 py-2">
           <div className="flex shrink-0 items-center gap-1">
             <button
               onClick={() =>
@@ -151,7 +151,7 @@ export function BugListPage() {
                 )}
               />
               Open
-              <span className="ml-0.5 rounded-full bg-muted px-1.5 py-0.5 text-xs tabular-nums leading-none">
+              <span className="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none tabular-nums">
                 {openCount}
               </span>
             </button>
@@ -180,7 +180,7 @@ export function BugListPage() {
                 )}
               />
               Closed
-              <span className="ml-0.5 rounded-full bg-muted px-1.5 py-0.5 text-xs tabular-nums leading-none">
+              <span className="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none tabular-nums">
                 {closedCount}
               </span>
             </button>
@@ -214,7 +214,7 @@ export function BugListPage() {
 
         {/* Bug rows */}
         {error && (
-          <p className="px-4 py-8 text-center text-sm text-destructive">
+          <p className="text-destructive px-4 py-8 text-center text-sm">
             Failed to load issues: {error.message}
           </p>
         )}
@@ -222,7 +222,7 @@ export function BugListPage() {
         {loading && !data && <BugListSkeleton />}
 
         {bugs?.nodes.length === 0 && (
-          <p className="px-4 py-8 text-center text-sm text-muted-foreground">
+          <p className="text-muted-foreground px-4 py-8 text-center text-sm">
             No {statusFilter} issues found.
           </p>
         )}
@@ -254,18 +254,18 @@ export function BugListPage() {
         ))}
 
         {totalPages > 1 && (
-          <div className="flex items-center justify-center gap-2 border-t border-border px-4 py-2">
+          <div className="border-border flex items-center justify-center gap-2 border-t px-4 py-2">
             <Button
               variant="ghost"
               size="sm"
               onClick={goPrev}
               disabled={!hasPrev || loading}
-              className="gap-1 text-muted-foreground"
+              className="text-muted-foreground gap-1"
             >
               <ChevronLeft className="size-4" />
               Previous
             </Button>
-            <span className="text-sm text-muted-foreground">
+            <span className="text-muted-foreground text-sm">
               Page {page + 1} of {totalPages}
             </span>
             <Button
@@ -273,7 +273,7 @@ export function BugListPage() {
               size="sm"
               onClick={goNext}
               disabled={!hasNext || loading}
-              className="gap-1 text-muted-foreground"
+              className="text-muted-foreground gap-1"
             >
               Next
               <ChevronRight className="size-4" />
@@ -373,7 +373,7 @@ function parseQueryString(input: string): {
 
 function BugListSkeleton() {
   return (
-    <div className="divide-y divide-border">
+    <div className="divide-border divide-y">
       {Array.from({ length: 8 }).map((_, i) => (
         <div key={i} className="flex items-start gap-3 px-4 py-3">
           <Skeleton className="mt-0.5 size-4 rounded-full" />

webui2/src/pages/CodePage.tsx πŸ”—

@@ -79,6 +79,31 @@ const BLOB_QUERY = gql`
   }
 `;
 
+interface RefsQueryData {
+  repository: {
+    name: string;
+    refs: { nodes: GitRef[] } | null;
+  } | null;
+}
+
+interface TreeQueryData {
+  repository: {
+    tree: GitTreeEntry[] | null;
+  } | null;
+}
+
+interface LastCommitsQueryData {
+  repository: {
+    lastCommits: GitLastCommit[] | null;
+  } | null;
+}
+
+interface BlobQueryData {
+  repository: {
+    blob: GitBlob | null;
+  } | null;
+}
+
 type ViewMode = "tree" | "blob" | "commits";
 
 export function CodePage() {
@@ -93,7 +118,7 @@ export function CodePage() {
     data: refsData,
     loading: refsLoading,
     error: refsError,
-  } = useQuery(REFS_QUERY, {
+  } = useQuery<RefsQueryData>(REFS_QUERY, {
     variables: { repo },
   });
   const refs: GitRef[] = refsData?.repository?.refs?.nodes ?? [];
@@ -116,14 +141,14 @@ export function CodePage() {
   const inTreeMode = viewMode === "tree" && !!currentRef;
   const inBlobMode = viewMode === "blob" && !!currentRef && !!currentPath;
 
-  const { data: treeData, loading: treeLoading } = useQuery(TREE_QUERY, {
+  const { data: treeData, loading: treeLoading } = useQuery<TreeQueryData>(TREE_QUERY, {
     variables: { repo, ref: currentRef, path: currentPath || null },
     skip: !inTreeMode,
   });
   const entries: GitTreeEntry[] = treeData?.repository?.tree ?? [];
 
   const entryNames = entries.map((e: GitTreeEntry) => e.name);
-  const { data: lastCommitsData } = useQuery(LAST_COMMITS_QUERY, {
+  const { data: lastCommitsData } = useQuery<LastCommitsQueryData>(LAST_COMMITS_QUERY, {
     variables: { repo, ref: currentRef, path: currentPath || null, names: entryNames },
     skip: !inTreeMode || entryNames.length === 0,
   });
@@ -135,7 +160,7 @@ export function CodePage() {
     lastCommit: lastCommitsByName.get(e.name)?.commit ?? undefined,
   }));
 
-  const { data: blobData, loading: blobLoading } = useQuery(BLOB_QUERY, {
+  const { data: blobData, loading: blobLoading } = useQuery<BlobQueryData>(BLOB_QUERY, {
     variables: { repo, ref: currentRef, path: currentPath },
     skip: !inBlobMode,
   });
@@ -149,7 +174,7 @@ export function CodePage() {
       ? `${currentPath}/${readmeEntry.name}`
       : readmeEntry.name
     : null;
-  const { data: readmeBlobData } = useQuery(BLOB_QUERY, {
+  const { data: readmeBlobData } = useQuery<BlobQueryData>(BLOB_QUERY, {
     variables: { repo, ref: currentRef, path: readmePath },
     skip: !inTreeMode || !readmePath,
   });
@@ -188,9 +213,9 @@ export function CodePage() {
   if (refsError) {
     return (
       <div className="flex flex-col items-center gap-3 py-16 text-center">
-        <AlertCircle className="size-8 text-muted-foreground" />
+        <AlertCircle className="text-muted-foreground size-8" />
         <p className="text-sm font-medium">Code browser unavailable</p>
-        <p className="max-w-sm text-xs text-muted-foreground">{refsError.message}</p>
+        <p className="text-muted-foreground max-w-sm text-xs">{refsError.message}</p>
       </div>
     );
   }
@@ -240,7 +265,7 @@ export function CodePage() {
           />
           {readme && (
             <div className="rounded-md border">
-              <div className="border-b px-4 py-2 text-xs font-medium text-muted-foreground">
+              <div className="text-muted-foreground border-b px-4 py-2 text-xs font-medium">
                 README
               </div>
               <div className="px-6 py-4">

webui2/src/pages/CommitPage.tsx πŸ”—

@@ -35,12 +35,30 @@ const COMMIT_QUERY = gql`
   }
 `;
 
+interface CommitQueryData {
+  repository: {
+    commit: {
+      hash: string;
+      shortHash: string;
+      message: string;
+      fullMessage: string;
+      authorName: string;
+      authorEmail: string | null;
+      date: string;
+      parents: string[];
+      files: {
+        nodes: { path: string; oldPath: string | null; status: string }[];
+      } | null;
+    } | null;
+  } | null;
+}
+
 export function CommitPage() {
   const { hash } = useParams<{ hash: string }>();
   const navigate = useNavigate();
   const repo = useRepo();
 
-  const { data, loading, error } = useQuery(COMMIT_QUERY, {
+  const { data, loading, error } = useQuery<CommitQueryData>(COMMIT_QUERY, {
     variables: { repo, hash },
     skip: !hash,
   });
@@ -49,7 +67,7 @@ export function CommitPage() {
 
   if (error) {
     return (
-      <div className="py-16 text-center text-sm text-destructive">
+      <div className="text-destructive py-16 text-center text-sm">
         Failed to load commit: {error.message}
       </div>
     );
@@ -64,43 +82,45 @@ export function CommitPage() {
   return (
     <div>
       <button
-        onClick={() => navigate(-1)}
-        className="mb-6 flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
+        onClick={() => {
+          void navigate(-1);
+        }}
+        className="text-muted-foreground hover:text-foreground mb-6 flex items-center gap-1.5 text-sm"
       >
         <ArrowLeft className="size-3.5" />
         Back
       </button>
 
-      <div className="mb-6 rounded-md border border-border p-5">
+      <div className="border-border mb-6 rounded-md border p-5">
         <div className="mb-1 flex items-start gap-3">
-          <GitCommit className="mt-1 size-5 shrink-0 text-muted-foreground" />
-          <h1 className="text-lg font-semibold leading-snug">{commit.message}</h1>
+          <GitCommit className="text-muted-foreground mt-1 size-5 shrink-0" />
+          <h1 className="text-lg leading-snug font-semibold">{commit.message}</h1>
         </div>
 
         {commit.fullMessage.includes("\n") && (
-          <pre className="mb-4 ml-8 mt-3 whitespace-pre-wrap font-sans text-sm text-muted-foreground">
+          <pre className="text-muted-foreground mt-3 mb-4 ml-8 font-sans text-sm whitespace-pre-wrap">
             {commit.fullMessage.split("\n").slice(1).join("\n").trim()}
           </pre>
         )}
 
-        <div className="ml-8 mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
+        <div className="text-muted-foreground mt-3 ml-8 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
           <span>
-            <span className="font-medium text-foreground">{commit.authorName}</span>
+            <span className="text-foreground font-medium">{commit.authorName}</span>
             {commit.authorEmail && <span> &lt;{commit.authorEmail}&gt;</span>}
           </span>
           <span title={date.toISOString()}>{format(date, "PPP")}</span>
         </div>
 
-        <div className="ml-8 mt-3 flex flex-wrap gap-3 text-xs">
+        <div className="mt-3 ml-8 flex flex-wrap gap-3 text-xs">
           <span className="text-muted-foreground">
-            commit <code className="font-mono text-foreground">{commit.hash}</code>
+            commit <code className="text-foreground font-mono">{commit.hash}</code>
           </span>
           {commit.parents.map((p: string) => (
             <span key={p} className="text-muted-foreground">
               parent{" "}
               <Link
                 to={repo ? `/${repo}/commit/${p}` : `/commit/${p}`}
-                className="font-mono text-foreground hover:underline"
+                className="text-foreground font-mono hover:underline"
               >
                 {p.slice(0, 7)}
               </Link>
@@ -110,12 +130,12 @@ export function CommitPage() {
       </div>
 
       <div>
-        <h2 className="mb-3 text-sm font-semibold text-muted-foreground">
+        <h2 className="text-muted-foreground mb-3 text-sm font-semibold">
           {files.length} file{files.length !== 1 ? "s" : ""} changed
         </h2>
-        <div className="divide-y divide-border overflow-hidden rounded-md border border-border">
+        <div className="divide-border border-border divide-y overflow-hidden rounded-md border">
           {files.length === 0 && (
-            <p className="px-4 py-4 text-sm text-muted-foreground">No file changes.</p>
+            <p className="text-muted-foreground px-4 py-4 text-sm">No file changes.</p>
           )}
           {files.map((file: { path: string; oldPath?: string | null; status: string }) => (
             <FileDiffView
@@ -136,12 +156,12 @@ function CommitPageSkeleton() {
   return (
     <div className="space-y-6">
       <Skeleton className="h-4 w-24" />
-      <div className="space-y-3 rounded-md border border-border p-5">
+      <div className="border-border space-y-3 rounded-md border p-5">
         <Skeleton className="h-6 w-3/4" />
         <Skeleton className="h-4 w-1/3" />
         <Skeleton className="h-3 w-1/2" />
       </div>
-      <div className="divide-y divide-border rounded-md border border-border">
+      <div className="divide-border border-border divide-y rounded-md border">
         {Array.from({ length: 5 }).map((_, i) => (
           <div key={i} className="flex items-center gap-3 px-4 py-2.5">
             <Skeleton className="size-4" />

webui2/src/pages/ErrorPage.tsx πŸ”—

@@ -24,9 +24,9 @@ export function ErrorPage() {
 
   return (
     <div className="flex min-h-screen flex-col items-center justify-center gap-4 text-center">
-      <AlertTriangle className="size-10 text-muted-foreground" />
+      <AlertTriangle className="text-muted-foreground size-10" />
       {status && <p className="text-5xl font-bold tracking-tight">{status}</p>}
-      <p className="text-sm text-muted-foreground">{message}</p>
+      <p className="text-muted-foreground text-sm">{message}</p>
       <Button variant="outline" size="sm" asChild>
         <Link to="/">Go home</Link>
       </Button>

webui2/src/pages/IdentitySelectPage.tsx πŸ”—

@@ -27,13 +27,17 @@ export function IdentitySelectPage() {
   const [working, setWorking] = useState(false);
 
   useEffect(() => {
-    void fetch("/auth/identities", { credentials: "include" })
-      .then((res) => {
+    async function loadIdentities() {
+      try {
+        const res = await fetch("/auth/identities", { credentials: "include" });
         if (!res.ok) throw new Error(`unexpected status ${res.status}`);
-        return res.json() as Promise<IdentityItem[]>;
-      })
-      .then(setIdentities)
-      .catch((e) => setError(String(e)));
+        const data: IdentityItem[] = await res.json();
+        setIdentities(data);
+      } catch (e) {
+        setError(String(e));
+      }
+    }
+    void loadIdentities();
   }, []);
 
   async function adopt(identityId: string | null) {
@@ -57,16 +61,16 @@ export function IdentitySelectPage() {
   return (
     <div className="mx-auto max-w-lg py-12">
       <div className="mb-2 flex items-center gap-3">
-        <UserCircle className="size-6 text-muted-foreground" />
+        <UserCircle className="text-muted-foreground size-6" />
         <h1 className="text-xl font-semibold">Choose your identity</h1>
       </div>
-      <p className="mb-8 text-sm text-muted-foreground">
+      <p className="text-muted-foreground mb-8 text-sm">
         No git-bug identity was found linked to your account. Select an existing identity to link
         it, or create a new one from your profile.
       </p>
 
       {error && (
-        <div className="mb-4 flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
+        <div className="border-destructive/30 bg-destructive/10 text-destructive mb-4 flex items-center gap-2 rounded-md border px-4 py-3 text-sm">
           <AlertCircle className="size-4 shrink-0" />
           {error}
         </div>
@@ -80,12 +84,12 @@ export function IdentitySelectPage() {
         </div>
       )}
 
-      <div className="divide-y divide-border rounded-md border border-border">
+      <div className="divide-border border-border divide-y rounded-md border">
         {identities?.map((id) => (
           <div key={id.id} className="flex items-center gap-3 px-4 py-3">
             <div className="min-w-0 flex-1">
               <p className="font-medium">{id.displayName}</p>
-              <p className="text-xs text-muted-foreground">
+              <p className="text-muted-foreground text-xs">
                 {id.login ? `@${id.login} Β· ` : ""}
                 {id.repoSlug} Β· {id.humanId}
               </p>
@@ -106,7 +110,7 @@ export function IdentitySelectPage() {
         <div className="flex items-center gap-3 px-4 py-3">
           <div className="min-w-0 flex-1">
             <p className="font-medium">Create new identity</p>
-            <p className="text-xs text-muted-foreground">
+            <p className="text-muted-foreground text-xs">
               A fresh git-bug identity will be created from your OAuth profile.
             </p>
           </div>

webui2/src/pages/NewBugPage.tsx πŸ”—

@@ -27,7 +27,7 @@ export function NewBugPage() {
     });
     const humanId = result.data?.bugCreate.bug.humanId;
     if (humanId) {
-      navigate(repo ? `/${repo}/issues/${humanId}` : `/issues/${humanId}`);
+      void navigate(repo ? `/${repo}/issues/${humanId}` : `/issues/${humanId}`);
     }
   }
 
@@ -37,7 +37,7 @@ export function NewBugPage() {
     <div className="mx-auto max-w-3xl">
       <Link
         to={issuesHref}
-        className="mb-6 flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
+        className="text-muted-foreground hover:text-foreground mb-6 flex items-center gap-1.5 text-sm"
       >
         <ArrowLeft className="size-3.5" />
         Back to issues
@@ -72,7 +72,7 @@ export function NewBugPage() {
               <button
                 type="button"
                 onClick={() => setPreview(false)}
-                className={`rounded px-2 py-0.5 transition-colors ${
+                className={`rounded-sm px-2 py-0.5 transition-colors ${
                   !preview ? "bg-muted font-medium" : "text-muted-foreground hover:text-foreground"
                 }`}
               >
@@ -82,7 +82,7 @@ export function NewBugPage() {
                 type="button"
                 onClick={() => setPreview(true)}
                 disabled={!message.trim()}
-                className={`rounded px-2 py-0.5 transition-colors disabled:opacity-40 ${
+                className={`rounded-sm px-2 py-0.5 transition-colors disabled:opacity-40 ${
                   preview ? "bg-muted font-medium" : "text-muted-foreground hover:text-foreground"
                 }`}
               >
@@ -92,7 +92,7 @@ export function NewBugPage() {
           </div>
 
           {preview ? (
-            <div className="min-h-[200px] rounded-md border border-input px-3 py-2">
+            <div className="border-input min-h-[200px] rounded-md border px-3 py-2">
               <Markdown content={message} />
             </div>
           ) : (
@@ -107,14 +107,16 @@ export function NewBugPage() {
         </div>
 
         {error && (
-          <p className="text-sm text-destructive">Failed to create issue: {error.message}</p>
+          <p className="text-destructive text-sm">Failed to create issue: {error.message}</p>
         )}
 
         <div className="flex justify-end gap-2">
           <Button
             type="button"
             variant="ghost"
-            onClick={() => navigate(issuesHref)}
+            onClick={() => {
+              void navigate(issuesHref);
+            }}
             disabled={loading}
           >
             Cancel

webui2/src/pages/RepoPickerPage.tsx πŸ”—

@@ -23,19 +23,19 @@ export function RepoPickerPage() {
   // Auto-redirect when there is exactly one repo β€” no need to pick.
   useEffect(() => {
     if (data?.repositories.nodes.length === 1) {
-      navigate("/" + repoSlug(data.repositories.nodes[0].name), { replace: true });
+      void navigate("/" + repoSlug(data.repositories.nodes[0].name), { replace: true });
     }
   }, [data, navigate]);
 
   return (
     <div className="mx-auto max-w-lg py-12">
       <div className="mb-8 flex items-center gap-3">
-        <GitFork className="size-6 text-muted-foreground" />
+        <GitFork className="text-muted-foreground size-6" />
         <h1 className="text-xl font-semibold">Repositories</h1>
       </div>
 
       {error && (
-        <div className="flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
+        <div className="border-destructive/30 bg-destructive/10 text-destructive flex items-center gap-2 rounded-md border px-4 py-3 text-sm">
           <AlertCircle className="size-4 shrink-0" />
           Failed to load repositories: {error.message}
         </div>
@@ -49,20 +49,20 @@ export function RepoPickerPage() {
         </div>
       )}
 
-      <div className="divide-y divide-border rounded-md border border-border">
+      <div className="divide-border border-border divide-y rounded-md border">
         {data?.repositories.nodes.map((repo) => (
           <Link
             key={repoSlug(repo.name)}
             to={`/${repoSlug(repo.name)}`}
-            className="flex items-center gap-3 px-4 py-4 transition-colors hover:bg-muted/50"
+            className="hover:bg-muted/50 flex items-center gap-3 px-4 py-4 transition-colors"
           >
-            <FolderOpen className="size-5 shrink-0 text-muted-foreground" />
-            <p className="font-medium text-foreground">{repoLabel(repo.name)}</p>
+            <FolderOpen className="text-muted-foreground size-5 shrink-0" />
+            <p className="text-foreground font-medium">{repoLabel(repo.name)}</p>
           </Link>
         ))}
 
         {data?.repositories.totalCount === 0 && (
-          <p className="px-4 py-8 text-center text-sm text-muted-foreground">
+          <p className="text-muted-foreground px-4 py-8 text-center text-sm">
             No repositories found.
           </p>
         )}

webui2/src/pages/UserProfilePage.tsx πŸ”—

@@ -61,7 +61,7 @@ export function UserProfilePage() {
 
   if (error) {
     return (
-      <div className="py-16 text-center text-sm text-destructive">
+      <div className="text-destructive py-16 text-center text-sm">
         Failed to load profile: {error.message}
       </div>
     );
@@ -71,7 +71,7 @@ export function UserProfilePage() {
 
   const identity = data?.repository?.identity;
   if (!identity) {
-    return <div className="py-16 text-center text-sm text-muted-foreground">User not found.</div>;
+    return <div className="text-muted-foreground py-16 text-center text-sm">User not found.</div>;
   }
 
   const openCount = data?.repository?.openCount.totalCount ?? 0;
@@ -97,7 +97,7 @@ export function UserProfilePage() {
     <div>
       <Link
         to={issuesHref}
-        className="mb-6 flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
+        className="text-muted-foreground hover:text-foreground mb-6 flex items-center gap-1.5 text-sm"
       >
         <ArrowLeft className="size-3.5" />
         Back to issues
@@ -118,11 +118,11 @@ export function UserProfilePage() {
             {/* isProtected means this identity has been cryptographically signed */}
             {identity.isProtected && (
               <span title="Protected identity">
-                <ShieldCheck className="size-4 text-muted-foreground" />
+                <ShieldCheck className="text-muted-foreground size-4" />
               </span>
             )}
           </div>
-          <div className="mt-1 space-y-0.5 text-sm text-muted-foreground">
+          <div className="text-muted-foreground mt-1 space-y-0.5 text-sm">
             {identity.login && <p>@{identity.login}</p>}
             {identity.email && <p>{identity.email}</p>}
             <p className="font-mono text-xs">#{identity.humanId}</p>
@@ -130,22 +130,22 @@ export function UserProfilePage() {
 
           {/* Aggregate stats β€” always visible, independent of selected tab */}
           <div className="mt-3 flex items-center gap-4 text-sm">
-            <span className="flex items-center gap-1 text-muted-foreground">
+            <span className="text-muted-foreground flex items-center gap-1">
               <CircleDot className="size-3.5 text-green-600 dark:text-green-400" />
-              <span className="font-medium text-foreground">{openCount}</span> open
+              <span className="text-foreground font-medium">{openCount}</span> open
             </span>
-            <span className="flex items-center gap-1 text-muted-foreground">
+            <span className="text-muted-foreground flex items-center gap-1">
               <CircleCheck className="size-3.5 text-purple-600 dark:text-purple-400" />
-              <span className="font-medium text-foreground">{closedCount}</span> closed
+              <span className="text-foreground font-medium">{closedCount}</span> closed
             </span>
           </div>
         </div>
       </div>
 
       {/* ── Issue list ─────────────────────────────────────────────────── */}
-      <div className="rounded-md border border-border">
+      <div className="border-border rounded-md border">
         {/* Open / Closed toggle β€” mirrors BugListPage style */}
-        <div className="flex items-center gap-1 border-b border-border px-4 py-2">
+        <div className="border-border flex items-center gap-1 border-b px-4 py-2">
           <button
             onClick={() => switchStatus("open")}
             className={cn(
@@ -162,7 +162,7 @@ export function UserProfilePage() {
               )}
             />
             Open
-            <span className="ml-0.5 rounded-full bg-muted px-1.5 py-0.5 text-xs leading-none">
+            <span className="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none">
               {openCount}
             </span>
           </button>
@@ -183,14 +183,14 @@ export function UserProfilePage() {
               )}
             />
             Closed
-            <span className="ml-0.5 rounded-full bg-muted px-1.5 py-0.5 text-xs leading-none">
+            <span className="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none">
               {closedCount}
             </span>
           </button>
         </div>
 
         {bugs?.nodes.length === 0 && (
-          <p className="px-4 py-8 text-center text-sm text-muted-foreground">
+          <p className="text-muted-foreground px-4 py-8 text-center text-sm">
             No {statusFilter} issues.
           </p>
         )}
@@ -201,7 +201,7 @@ export function UserProfilePage() {
           return (
             <div
               key={bug.id}
-              className="flex items-start gap-3 border-b border-border px-4 py-3 last:border-0"
+              className="border-border flex items-start gap-3 border-b px-4 py-3 last:border-0"
             >
               <StatusIcon
                 className={cn(
@@ -215,7 +215,7 @@ export function UserProfilePage() {
                 <div className="flex flex-wrap items-baseline gap-2">
                   <Link
                     to={repo ? `/${repo}/issues/${bug.humanId}` : `/issues/${bug.humanId}`}
-                    className="font-medium text-foreground hover:text-primary hover:underline"
+                    className="text-foreground hover:text-primary font-medium hover:underline"
                   >
                     {bug.title}
                   </Link>
@@ -223,13 +223,13 @@ export function UserProfilePage() {
                     <LabelBadge key={label.name} name={label.name} color={label.color} />
                   ))}
                 </div>
-                <p className="mt-0.5 text-xs text-muted-foreground">
+                <p className="text-muted-foreground mt-0.5 text-xs">
                   #{bug.humanId} opened{" "}
                   {formatDistanceToNow(new Date(bug.createdAt), { addSuffix: true })}
                 </p>
               </div>
               {bug.comments.totalCount > 0 && (
-                <div className="flex shrink-0 items-center gap-1 text-xs text-muted-foreground">
+                <div className="text-muted-foreground flex shrink-0 items-center gap-1 text-xs">
                   <MessageSquare className="size-3.5" />
                   {bug.comments.totalCount}
                 </div>
@@ -240,18 +240,18 @@ export function UserProfilePage() {
 
         {/* Pagination footer β€” only shown when there is more than one page */}
         {totalPages > 1 && (
-          <div className="flex items-center justify-center gap-2 border-t border-border px-4 py-2">
+          <div className="border-border flex items-center justify-center gap-2 border-t px-4 py-2">
             <Button
               variant="ghost"
               size="sm"
               onClick={goPrev}
               disabled={!hasPrev || loading}
-              className="gap-1 text-muted-foreground"
+              className="text-muted-foreground gap-1"
             >
               <ChevronLeft className="size-4" />
               Previous
             </Button>
-            <span className="text-sm text-muted-foreground">
+            <span className="text-muted-foreground text-sm">
               Page {page + 1} of {totalPages}
             </span>
             <Button
@@ -259,7 +259,7 @@ export function UserProfilePage() {
               size="sm"
               onClick={goNext}
               disabled={!hasNext || loading}
-              className="gap-1 text-muted-foreground"
+              className="text-muted-foreground gap-1"
             >
               Next
               <ChevronRight className="size-4" />

webui2/tailwind.config.ts πŸ”—

@@ -1,71 +0,0 @@
-import type { Config } from "tailwindcss";
-
-const config: Config = {
-  darkMode: ["class"],
-  content: ["./index.html", "./src/**/*.{ts,tsx}"],
-  theme: {
-    extend: {
-      fontFamily: {
-        sans: ["ui-sans-serif", "system-ui", "sans-serif"],
-        mono: ["ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "monospace"],
-      },
-      colors: {
-        border: "hsl(var(--border))",
-        input: "hsl(var(--input))",
-        ring: "hsl(var(--ring))",
-        background: "hsl(var(--background))",
-        foreground: "hsl(var(--foreground))",
-        primary: {
-          DEFAULT: "hsl(var(--primary))",
-          foreground: "hsl(var(--primary-foreground))",
-        },
-        secondary: {
-          DEFAULT: "hsl(var(--secondary))",
-          foreground: "hsl(var(--secondary-foreground))",
-        },
-        destructive: {
-          DEFAULT: "hsl(var(--destructive))",
-          foreground: "hsl(var(--destructive-foreground))",
-        },
-        muted: {
-          DEFAULT: "hsl(var(--muted))",
-          foreground: "hsl(var(--muted-foreground))",
-        },
-        accent: {
-          DEFAULT: "hsl(var(--accent))",
-          foreground: "hsl(var(--accent-foreground))",
-        },
-        popover: {
-          DEFAULT: "hsl(var(--popover))",
-          foreground: "hsl(var(--popover-foreground))",
-        },
-        card: {
-          DEFAULT: "hsl(var(--card))",
-          foreground: "hsl(var(--card-foreground))",
-        },
-      },
-      borderRadius: {
-        lg: "var(--radius)",
-        md: "calc(var(--radius) - 2px)",
-        sm: "calc(var(--radius) - 4px)",
-      },
-      keyframes: {
-        "accordion-down": {
-          from: { height: "0" },
-          to: { height: "var(--radix-accordion-content-height)" },
-        },
-        "accordion-up": {
-          from: { height: "var(--radix-accordion-content-height)" },
-          to: { height: "0" },
-        },
-      },
-      animation: {
-        "accordion-down": "accordion-down 0.2s ease-out",
-        "accordion-up": "accordion-up 0.2s ease-out",
-      },
-    },
-  },
-  plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
-};
-
-export default config;

webui2/tsconfig.node.json πŸ”—

@@ -13,5 +13,5 @@
     "noUnusedParameters": true,
     "noFallthroughCasesInSwitch": true
   },
-  "include": ["vite.config.ts", "tailwind.config.ts", "codegen.ts"]
+  "include": ["vite.config.ts", "codegen.ts"]
 }

webui2/vite.config.ts πŸ”—

@@ -1,5 +1,6 @@
 import path from "path";
 
+import tailwindcss from "@tailwindcss/vite";
 import react from "@vitejs/plugin-react";
 import { defineConfig } from "vite";
 
@@ -7,7 +8,7 @@ import { defineConfig } from "vite";
 const API_URL = process.env.VITE_API_URL || "http://localhost:3000";
 
 export default defineConfig({
-  plugins: [react()],
+  plugins: [tailwindcss(), react()],
   resolve: {
     alias: {
       "@": path.resolve(__dirname, "./src"),