diff --git a/browser-extensions/chrome/src/app/App.tsx b/browser-extensions/chrome/src/app/App.tsx
index ca682ab0f..3fa389cf2 100644
--- a/browser-extensions/chrome/src/app/App.tsx
+++ b/browser-extensions/chrome/src/app/App.tsx
@@ -9,12 +9,13 @@ import CredentialsList from './pages/CredentialsList';
import EmailsList from './pages/EmailsList';
import LoadingSpinner from './components/LoadingSpinner';
import Home from './pages/Home';
-import './style.css';
import CredentialDetails from './pages/CredentialDetails';
import EmailDetails from './pages/EmailDetails';
import Settings from './pages/Settings';
import GlobalStateChangeHandler from './components/GlobalStateChangeHandler';
import { useLoading } from './context/LoadingContext';
+import Logout from './pages/Logout';
+import './style.css';
/**
* Route configuration.
@@ -44,6 +45,7 @@ const App: React.FC = () => {
{ path: '/emails', element: , showBackButton: false },
{ path: '/emails/:id', element: , showBackButton: true, title: 'Email details' },
{ path: '/settings', element: , showBackButton: false },
+ { path: '/logout', element: , showBackButton: false },
];
useEffect(() => {
diff --git a/browser-extensions/chrome/src/app/components/Layout/UserMenu.tsx b/browser-extensions/chrome/src/app/components/Layout/UserMenu.tsx
index d14752c62..f171ba087 100644
--- a/browser-extensions/chrome/src/app/components/Layout/UserMenu.tsx
+++ b/browser-extensions/chrome/src/app/components/Layout/UserMenu.tsx
@@ -47,10 +47,7 @@ export const UserMenu: React.FC = () => {
*/
const onLogout = async () : Promise => {
showLoading();
- await authContext.logout();
- navigate('/', { replace: true });
- // Delay for 100ms for improved UX
- await new Promise(resolve => setTimeout(resolve, 100));
+ navigate('/logout', { replace: true });
hideLoading();
};
diff --git a/browser-extensions/chrome/src/app/context/DbContext.tsx b/browser-extensions/chrome/src/app/context/DbContext.tsx
index 81170725d..72a8f6a83 100644
--- a/browser-extensions/chrome/src/app/context/DbContext.tsx
+++ b/browser-extensions/chrome/src/app/context/DbContext.tsx
@@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect, useCallback, use
import SqliteClient from '../../shared/SqliteClient';
import { VaultResponse } from '../../shared/types/webapi/VaultResponse';
import EncryptionUtility from '../../shared/EncryptionUtility';
+
type DbContextType = {
sqliteClient: SqliteClient | null;
dbInitialized: boolean;
diff --git a/browser-extensions/chrome/src/app/context/WebApiContext.tsx b/browser-extensions/chrome/src/app/context/WebApiContext.tsx
index 21e813f9c..65f7b6e92 100644
--- a/browser-extensions/chrome/src/app/context/WebApiContext.tsx
+++ b/browser-extensions/chrome/src/app/context/WebApiContext.tsx
@@ -16,7 +16,13 @@ export const WebApiProvider: React.FC<{ children: React.ReactNode }> = ({ childr
*/
useEffect(() : void => {
const service = new WebApiService(
- logout
+ (statusError: string | null) => {
+ if (statusError) {
+ logout(statusError);
+ } else {
+ logout();
+ }
+ }
);
setWebApiService(service);
}, [logout]);
diff --git a/browser-extensions/chrome/src/app/pages/CredentialsList.tsx b/browser-extensions/chrome/src/app/pages/CredentialsList.tsx
index 00f43bbf0..56549ce86 100644
--- a/browser-extensions/chrome/src/app/pages/CredentialsList.tsx
+++ b/browser-extensions/chrome/src/app/pages/CredentialsList.tsx
@@ -7,7 +7,6 @@ import { useLoading } from '../context/LoadingContext';
import { useWebApi } from '../context/WebApiContext';
import { VaultResponse } from '../../shared/types/webapi/VaultResponse';
import ReloadButton from '../components/ReloadButton';
-import { useAuth } from '../context/AuthContext';
import LoadingSpinner from '../components/LoadingSpinner';
import { useMinDurationLoading } from '../hooks/useMinDurationLoading';
@@ -21,7 +20,6 @@ const CredentialsList: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const navigate = useNavigate();
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
- const authContext = useAuth();
/**
* Loading state with minimum duration for more fluid UX.
@@ -40,7 +38,7 @@ const CredentialsList: React.FC = () => {
const statusResponse = await webApi.getStatus();
const statusError = webApi.validateStatusResponse(statusResponse);
if (statusError !== null) {
- authContext.logout(statusError);
+ await webApi.logout(statusError);
return;
}
@@ -60,7 +58,7 @@ const CredentialsList: React.FC = () => {
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
if (vaultError) {
- authContext.logout(vaultError);
+ await webApi.logout(vaultError);
hideLoading();
return;
}
@@ -73,7 +71,7 @@ const CredentialsList: React.FC = () => {
} catch (err) {
console.error('Refresh error:', err);
}
- }, [dbContext, webApi, authContext, hideLoading]);
+ }, [dbContext, webApi, hideLoading]);
/**
* Manually refresh the credentials list.
diff --git a/browser-extensions/chrome/src/app/pages/Logout.tsx b/browser-extensions/chrome/src/app/pages/Logout.tsx
new file mode 100644
index 000000000..9a2c253f8
--- /dev/null
+++ b/browser-extensions/chrome/src/app/pages/Logout.tsx
@@ -0,0 +1,32 @@
+import React, { useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import { useWebApi } from '../context/WebApiContext';
+
+/**
+ * Logout page.
+ */
+const Logout: React.FC = () => {
+ const authContext = useAuth();
+ const webApi = useWebApi();
+ const navigate = useNavigate();
+ /**
+ * Logout and navigate to home page.
+ */
+ useEffect(() => {
+ /**
+ * Perform logout via async method to ensure logout is completed before navigating to home page.
+ */
+ const performLogout = async () : Promise => {
+ await webApi.logout();
+ navigate('/');
+ };
+
+ performLogout();
+ }, [authContext, navigate, webApi]);
+
+ // Return null since this is just a functional component that handles logout.
+ return null;
+};
+
+export default Logout;
diff --git a/browser-extensions/chrome/src/app/pages/Unlock.tsx b/browser-extensions/chrome/src/app/pages/Unlock.tsx
index 1d1972d11..5799fe5c4 100644
--- a/browser-extensions/chrome/src/app/pages/Unlock.tsx
+++ b/browser-extensions/chrome/src/app/pages/Unlock.tsx
@@ -31,7 +31,7 @@ const Unlock: React.FC = () => {
const statusResponse = await webApi.getStatus();
const statusError = webApi.validateStatusResponse(statusResponse);
if (statusError !== null) {
- authContext.logout(statusError);
+ await webApi.logout(statusError);
}
};
@@ -81,26 +81,6 @@ const Unlock: React.FC = () => {
}
};
- /**
- * Handle logout
- */
- const handleLogout = async () : Promise => {
- showLoading();
- try {
- await webApi.logout();
- } catch (err) {
- console.error('Logout error:', err);
- }
-
- try {
- await authContext.logout();
- } catch (err) {
- console.error('Logout error:', err);
- } finally {
- hideLoading();
- }
- };
-
return (
diff --git a/browser-extensions/chrome/src/contentScript/Form.ts b/browser-extensions/chrome/src/contentScript/Form.ts
index 6246af10b..af07fb92d 100644
--- a/browser-extensions/chrome/src/contentScript/Form.ts
+++ b/browser-extensions/chrome/src/contentScript/Form.ts
@@ -354,7 +354,7 @@ function triggerInputEvents(element: HTMLInputElement | HTMLSelectElement) : voi
const rect = element.getBoundingClientRect();
overlay.style.cssText = `
position: fixed;
- z-index: 999999999;
+ z-index: 999999991;
pointer-events: none;
top: ${rect.top}px;
left: ${rect.left}px;
diff --git a/browser-extensions/chrome/src/contentScript/Popup.ts b/browser-extensions/chrome/src/contentScript/Popup.ts
index 321e0f251..a7cc8a843 100644
--- a/browser-extensions/chrome/src/contentScript/Popup.ts
+++ b/browser-extensions/chrome/src/contentScript/Popup.ts
@@ -34,7 +34,7 @@ export function createBasePopup(input: HTMLInputElement) : HTMLElement {
popup.style.cssText = `
all: unset;
position: absolute;
- z-index: 999999999;
+ z-index: 999999991;
background: ${isDarkMode() ? '#1f2937' : 'white'};
border: 1px solid ${isDarkMode() ? '#374151' : '#ccc'};
border-radius: 4px;
@@ -228,7 +228,14 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
createButton.style.backgroundColor = isDarkMode() ? '#374151' : '#f3f4f6';
});
- createButton.addEventListener('click', async () => {
+ /**
+ * Handle create button click
+ */
+ const handleCreateClick = async (e: Event) : Promise => {
+ e.preventDefault();
+ e.stopPropagation();
+ e.stopImmediatePropagation();
+
// Determine service name based on conditions
let suggestedName = document.title;
@@ -331,7 +338,7 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
};
chrome.runtime.sendMessage({ type: 'CREATE_IDENTITY', credential }, () => {
- // Refresh the popup to show new identity
+ // Refresh the popup to show new identity.
openAutofillPopup(input);
});
} catch (error) {
@@ -345,9 +352,32 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
removeExistingPopup();
}, 2000);
}
+ };
+
+ // Add click listener with capture and prevent removal.
+ createButton.addEventListener('click', handleCreateClick, {
+ capture: true,
+ passive: false
});
- // Create search input instead of button
+ // Backup click handling using mousedown/mouseup if needed.
+ let isMouseDown = false;
+ createButton.addEventListener('mousedown', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ isMouseDown = true;
+ }, { capture: true });
+
+ createButton.addEventListener('mouseup', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (isMouseDown) {
+ handleCreateClick(e);
+ }
+ isMouseDown = false;
+ }, { capture: true });
+
+ // Create search input.
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.dataset.aliasvaultIgnore = 'true';
@@ -370,7 +400,7 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
text-align: left;
`;
- // Add focus styles
+ // Add focus styles.
searchInput.addEventListener('focus', () => {
searchInput.style.borderColor = '#2563eb';
searchInput.style.boxShadow = '0 0 0 2px rgba(37, 99, 235, 0.2)';
@@ -381,7 +411,7 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
searchInput.style.boxShadow = 'none';
});
- // Handle search input
+ // Handle search input.
let searchTimeout: NodeJS.Timeout;
searchInput.addEventListener('input', () => {
@@ -654,8 +684,11 @@ function createCredentialList(credentials: Credential[], input: HTMLInputElement
// Handle base64 image data
if (cred.Logo) {
try {
- const base64Logo = base64Encode(cred.Logo as Uint8Array);
- imgElement.src = `data:image/x-icon;base64,${base64Logo}`;
+ const logoBytes = toUint8Array(cred.Logo);
+ const base64Logo = base64Encode(logoBytes);
+ // Detect image type from first few bytes
+ const mimeType = detectMimeType(logoBytes);
+ imgElement.src = `data:${mimeType};base64,${base64Logo}`;
} catch (error) {
console.error('Error setting logo:', error);
imgElement.src = `data:image/x-icon;base64,${placeholderBase64}`;
@@ -837,7 +870,7 @@ export async function createEditNamePopup(defaultName: string): Promise data + String.fromCharCode(byte), ''));
} catch (error) {
console.error('Error encoding to base64:', error);
@@ -1115,3 +1158,42 @@ async function getFaviconBytes(document: Document): Promise {
return null; // Return null if no favicon could be downloaded
}
+
+/**
+ * Detect MIME type from file signature (magic numbers)
+ */
+function detectMimeType(bytes: Uint8Array): string {
+ /**
+ * Check if the file is an SVG file.
+ */
+ const isSvg = () : boolean => {
+ const header = new TextDecoder().decode(bytes.slice(0, 5)).toLowerCase();
+ return header.includes(' {
+ return bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 && bytes[3] === 0x00;
+ };
+
+ /**
+ * Check if the file is an PNG file.
+ */
+ const isPng = () : boolean => {
+ return bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47;
+ };
+
+ if (isSvg()) {
+ return 'image/svg+xml';
+ }
+ if (isIco()) {
+ return 'image/x-icon';
+ }
+ if (isPng()) {
+ return 'image/png';
+ }
+
+ return 'image/x-icon';
+}
diff --git a/browser-extensions/chrome/src/shared/WebApiService.ts b/browser-extensions/chrome/src/shared/WebApiService.ts
index c0af80d88..6b1f667b5 100644
--- a/browser-extensions/chrome/src/shared/WebApiService.ts
+++ b/browser-extensions/chrome/src/shared/WebApiService.ts
@@ -19,9 +19,9 @@ export class WebApiService {
/**
* Constructor for the WebApiService class.
*
- * @param {Function} handleLogout - Function to handle logout.
+ * @param {Function} authContextLogout - Function to handle logout.
*/
- public constructor(private readonly handleLogout: () => void) { }
+ public constructor(private readonly authContextLogout: (statusError: string | null) => void) { }
/**
* Get the base URL for the API from settings.
@@ -79,7 +79,7 @@ export class WebApiService {
return parseJson ? retryResponse.json() : retryResponse as unknown as T;
} else {
- this.handleLogout();
+ this.authContextLogout(null);
throw new Error('Session expired');
}
}
@@ -106,11 +106,13 @@ export class WebApiService {
try {
const baseUrl = await this.getBaseUrl();
+
const response = await fetch(`${baseUrl}Auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Ignore-Failure': 'true',
+ 'X-AliasVault-Client': `${AppInfo.CLIENT_NAME}-${AppInfo.VERSION}`,
},
body: JSON.stringify({
token: await this.getAccessToken(),
@@ -126,7 +128,7 @@ export class WebApiService {
this.updateTokens(tokenResponse.token, tokenResponse.refreshToken);
return tokenResponse.token;
} catch {
- this.handleLogout();
+ this.authContextLogout('Your session has expired. Please login again.');
return null;
}
}
@@ -146,7 +148,7 @@ export class WebApiService {
const response = await this.fetch(endpoint, {
method: 'GET',
headers: {
- 'Accept': 'application/octet-stream'
+ 'Accept': 'application/octet-stream',
}
}, false);
@@ -197,18 +199,26 @@ export class WebApiService {
}
/**
- * Logout and revoke tokens via WebApi.
+ * Logout and revoke tokens via WebApi and remove local storage tokens via AuthContext.
*/
- public async logout(): Promise {
- const refreshToken = await this.getRefreshToken();
- if (!refreshToken) {
- return;
+ public async logout(statusError: string | null = null): Promise {
+ // Logout and revoke tokens via WebApi.
+ try {
+ const refreshToken = await this.getRefreshToken();
+ if (!refreshToken) {
+ return;
+ }
+
+ await this.post('Auth/revoke', {
+ token: await this.getAccessToken(),
+ refreshToken: refreshToken,
+ }, false);
+ } catch (err) {
+ console.error('WebApi logout error:', err);
}
- await this.post('Auth/revoke', {
- token: await this.getAccessToken(),
- refreshToken: refreshToken,
- }, false);
+ // Logout and remove tokens from local storage via AuthContext.
+ this.authContextLogout(statusError);
}
/**
@@ -308,7 +318,7 @@ export class WebApiService {
/**
* When the reader has finished loading, convert the result to a Base64 string.
*/
- reader.onloadend = () : void => {
+ reader.onloadend = (): void => {
const result = reader.result;
if (typeof result === 'string') {
resolve(result.split(',')[1]); // Remove the data URL prefix
@@ -320,7 +330,7 @@ export class WebApiService {
/**
* If the reader encounters an error, reject the promise with a proper Error object.
*/
- reader.onerror = () : void => {
+ reader.onerror = (): void => {
reject(new Error('Failed to read blob as Data URL'));
};
reader.readAsDataURL(blob);
diff --git a/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts b/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts
index 79fa12ffe..946cb12a3 100644
--- a/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts
+++ b/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts
@@ -101,15 +101,33 @@ export class FormDetector {
}
}
- // Check for parent label
+ // Check for parent label and table cell structure
let currentElement = input;
for (let i = 0; i < 3; i++) {
+ // Check for parent label
const parentLabel = currentElement.closest('label');
if (parentLabel) {
attributes.push(parentLabel.textContent?.toLowerCase() ?? '');
break;
}
+ // Check for table cell structure
+ const parentTd = currentElement.closest('td');
+ if (parentTd) {
+ // Get the parent row
+ const parentTr = parentTd.closest('tr');
+ if (parentTr) {
+ // Check all sibling cells in the row
+ const siblingTds = parentTr.querySelectorAll('td');
+ for (const td of siblingTds) {
+ if (td !== parentTd) { // Skip the cell containing the input
+ attributes.push(td.textContent?.toLowerCase() ?? '');
+ }
+ }
+ }
+ break; // Found table structure, no need to continue up the tree
+ }
+
if (currentElement.parentElement) {
currentElement = currentElement.parentElement as HTMLInputElement;
} else {
diff --git a/browser-extensions/chrome/src/shared/formDetector/__tests__/FormDetector.en.test.ts b/browser-extensions/chrome/src/shared/formDetector/__tests__/FormDetector.en.test.ts
index c694de033..a90656b4e 100644
--- a/browser-extensions/chrome/src/shared/formDetector/__tests__/FormDetector.en.test.ts
+++ b/browser-extensions/chrome/src/shared/formDetector/__tests__/FormDetector.en.test.ts
@@ -44,6 +44,13 @@ describe('FormDetector English tests', () => {
testField(FormField.LastName, 'fbclc_lName', htmlFile);
});
+ describe('English registration form 5 detection', () => {
+ const htmlFile = 'en-registration-form5.html';
+
+ testField(FormField.Username, 'aliasvault-input-7owmnahd9', htmlFile);
+ testField(FormField.Password, 'aliasvault-input-ienw3qgxv', htmlFile);
+ });
+
describe('English email form 1 detection', () => {
const htmlFile = 'en-email-form1.html';
diff --git a/browser-extensions/chrome/src/shared/formDetector/__tests__/test-forms/en-registration-form5.html b/browser-extensions/chrome/src/shared/formDetector/__tests__/test-forms/en-registration-form5.html
new file mode 100644
index 000000000..2e52e3cfd
--- /dev/null
+++ b/browser-extensions/chrome/src/shared/formDetector/__tests__/test-forms/en-registration-form5.html
@@ -0,0 +1,6 @@
+Login
+ Forgot your password?
+ Create Account
+
\ No newline at end of file
diff --git a/docs/misc/dev/browser-extensions.md b/docs/misc/dev/browser-extensions.md
index 7aa720a1f..0a96d2644 100644
--- a/docs/misc/dev/browser-extensions.md
+++ b/docs/misc/dev/browser-extensions.md
@@ -56,3 +56,5 @@ The following websites have been known to cause issues in the past (but should b
| --- | --- |
| https://www.paprika-shopping.nl/nieuwsbrief/newsletter-register-landing.html | Popup CSS style conflicts |
| https://bloshing.com/inschrijven-nieuwsbrief | Popup CSS style conflicts |
+| https://gamefaqs.gamespot.com/user | Popup buttons not working |
+| https://news.ycombinator.com/login?goto=news | Popup and client favicon not showing due to SVG format |
diff --git a/src/AliasVault.Client/Main/Components/Credentials/DisplayFavicon.razor b/src/AliasVault.Client/Main/Components/Credentials/DisplayFavicon.razor
index a3186fc28..80959d9ee 100644
--- a/src/AliasVault.Client/Main/Components/Credentials/DisplayFavicon.razor
+++ b/src/AliasVault.Client/Main/Components/Credentials/DisplayFavicon.razor
@@ -26,6 +26,9 @@ else
[Parameter]
public bool Padding { get; set; }
+ ///
+ /// The data URL of the favicon.
+ ///
private string? _faviconDataUrl;
///
@@ -33,8 +36,46 @@ else
{
if (FaviconBytes is not null)
{
+ string mimeType = DetectMimeType(FaviconBytes);
string base64String = Convert.ToBase64String(FaviconBytes);
- _faviconDataUrl = $"data:image/x-icon;base64,{base64String}";
+ _faviconDataUrl = $"data:{mimeType};base64,{base64String}";
}
}
+
+ ///
+ /// Detect the mime type of the favicon.
+ ///
+ /// The bytes of the favicon.
+ /// The mime type of the favicon.
+ private static string DetectMimeType(byte[] bytes)
+ {
+ // Check for SVG.
+ if (bytes.Length >= 5)
+ {
+ string header = System.Text.Encoding.ASCII.GetString(bytes.Take(5).ToArray()).ToLower();
+ if (header.Contains("= 4 &&
+ bytes[0] == 0x00 && bytes[1] == 0x00 &&
+ bytes[2] == 0x01 && bytes[3] == 0x00)
+ {
+ return "image/x-icon";
+ }
+
+ // Check for PNG.
+ if (bytes.Length >= 4 &&
+ bytes[0] == 0x89 && bytes[1] == 0x50 &&
+ bytes[2] == 0x4E && bytes[3] == 0x47)
+ {
+ return "image/png";
+ }
+
+ // Default to x-icon if unknown.
+ return "image/x-icon";
+ }
}
diff --git a/src/AliasVault.Client/wwwroot/css/tailwind.css b/src/AliasVault.Client/wwwroot/css/tailwind.css
index f175c3c1a..711b704a5 100644
--- a/src/AliasVault.Client/wwwroot/css/tailwind.css
+++ b/src/AliasVault.Client/wwwroot/css/tailwind.css
@@ -1342,6 +1342,11 @@ video {
border-color: rgb(191 219 254 / var(--tw-border-opacity));
}
+.border-blue-300 {
+ --tw-border-opacity: 1;
+ border-color: rgb(147 197 253 / var(--tw-border-opacity));
+}
+
.border-blue-700 {
--tw-border-opacity: 1;
border-color: rgb(29 78 216 / var(--tw-border-opacity));
@@ -1392,6 +1397,16 @@ video {
border-color: rgb(239 68 68 / var(--tw-border-opacity));
}
+.border-blue-200 {
+ --tw-border-opacity: 1;
+ border-color: rgb(191 219 254 / var(--tw-border-opacity));
+}
+
+.border-gray-100 {
+ --tw-border-opacity: 1;
+ border-color: rgb(243 244 246 / var(--tw-border-opacity));
+}
+
.bg-amber-50 {
--tw-bg-opacity: 1;
background-color: rgb(255 251 235 / var(--tw-bg-opacity));
@@ -1729,6 +1744,10 @@ video {
vertical-align: middle;
}
+.align-text-bottom {
+ vertical-align: text-bottom;
+}
+
.font-mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
}
@@ -1942,6 +1961,11 @@ video {
color: rgb(133 77 14 / var(--tw-text-opacity));
}
+.text-primary-800 {
+ --tw-text-opacity: 1;
+ color: rgb(154 93 38 / var(--tw-text-opacity));
+}
+
.opacity-0 {
opacity: 0;
}
@@ -2322,6 +2346,11 @@ video {
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}
+.dark\:border-blue-800:is(.dark *) {
+ --tw-border-opacity: 1;
+ border-color: rgb(30 64 175 / var(--tw-border-opacity));
+}
+
.dark\:border-gray-400:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(156 163 175 / var(--tw-border-opacity));
@@ -2381,6 +2410,11 @@ video {
background-color: rgb(30 58 138 / 0.2);
}
+.dark\:bg-blue-900\/50:is(.dark *) {
+ background-color: rgb(30 58 138 / 0.5);
+
+}
+
.dark\:bg-gray-500:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
@@ -2465,6 +2499,15 @@ video {
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
}
+.dark\:bg-blue-900\/20:is(.dark *) {
+ background-color: rgb(30 58 138 / 0.2);
+}
+
+.dark\:bg-primary-900:is(.dark *) {
+ --tw-bg-opacity: 1;
+ background-color: rgb(123 74 30 / var(--tw-bg-opacity));
+}
+
.dark\:bg-opacity-80:is(.dark *) {
--tw-bg-opacity: 0.8;
}