├── .eslintignore ├── src ├── components │ ├── EditSigners.tsx │ ├── InfoBox.tsx │ ├── Contexts.ts │ ├── GenericField.tsx │ ├── AddressView.tsx │ ├── NetworkSwitcher.tsx │ ├── DataActionPreview.tsx │ ├── SafeInformation.tsx │ └── SetOwnerModal.tsx ├── vite-env.d.ts ├── app │ ├── Error.tsx │ ├── SafeInformationPage.tsx │ ├── Wrapper.tsx │ ├── App.tsx │ ├── ViewSafe.tsx │ ├── Root.tsx │ ├── CreateSafe.tsx │ └── NewSafeProposal.tsx ├── main.tsx ├── utils │ ├── validators.ts │ ├── etherFormatting.ts │ └── get.ts ├── hooks │ ├── useUpdateProposalViaQuery.ts │ ├── useSetParamsFromQuery.ts │ └── useLoadProposalFromQuery.ts ├── schemas │ └── proposal.ts ├── chains.ts └── app.css ├── .npmrc ├── .gitignore ├── postcss.config.js ├── index.html ├── vite.config.ts ├── tsconfig.node.json ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── README.md ├── .eslintrc.cjs ├── tsconfig.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/components/EditSigners.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .parcel-cache 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export { config as default } from "reshaped/config/postcss.js"; -------------------------------------------------------------------------------- /src/app/Error.tsx: -------------------------------------------------------------------------------- 1 | export const Error = () => { 2 | return
ERR
; 3 | }; 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Smol Safe 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | define: { 8 | 'process.env': {}, 9 | 'global': {} 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@master 12 | 13 | - run: corepack enable 14 | - run: pnpm install --frozen-lockfile 15 | 16 | - name: Build 🏗️ 17 | run: pnpm build 18 | -------------------------------------------------------------------------------- /src/components/InfoBox.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, Tooltip } from "reshaped"; 2 | import { Info } from "react-feather"; 3 | 4 | export const InfoBox = ({ children }: { children: React.ReactNode }) => { 5 | return ( 6 | 7 | {(attributes) => } />} 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { Wrapper } from "./app/Wrapper"; 4 | import "reshaped/themes/reshaped/theme.css"; 5 | import { Buffer } from "buffer"; 6 | globalThis.Buffer = Buffer; 7 | 8 | const container = document.getElementById("app"); 9 | const root = createRoot(container!); 10 | root.render( 11 | 12 | 13 | , 14 | ); 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | small on-chain front-end for gnosis safe 2 | 3 | [use it ↝](https://ourzora.github.io/smol-safe/) 4 | 5 | this allows for gnosis safe interactions without requiring any specific backend-servers etc. utilizing safe protocol kit and a web3 injected connector. it's especially useful for new chains that may not be fully integrated into the official gnosis safe UI or as a backup user interface. All signatures in this app are created on-chain. 6 | 7 | contributions are welcome. to add a new chain that is not yet in gnosis-deploys, update the chains.ts file. 8 | -------------------------------------------------------------------------------- /src/utils/validators.ts: -------------------------------------------------------------------------------- 1 | import { isAddress, parseEther } from "viem"; 2 | import { string } from "yup"; 3 | 4 | export const validateAddress = (address: string) => { 5 | if (!isAddress(address)) { 6 | return "Invalid address"; 7 | } 8 | }; 9 | 10 | export const validateETH = (value: string) => { 11 | try { 12 | parseEther(value); 13 | } catch (e: any) { 14 | return "ETH Value is Invalid"; 15 | } 16 | }; 17 | 18 | export const yupAddress = string() 19 | .matches(/^0x[a-fA-F0-9]{40}$/, "Needs to be a valid address") 20 | .required(); 21 | -------------------------------------------------------------------------------- /src/components/Contexts.ts: -------------------------------------------------------------------------------- 1 | import Safe from "@safe-global/protocol-kit"; 2 | import { BrowserProvider } from "ethers"; 3 | import { Address } from "viem"; 4 | 5 | export type SafeInformationType = { 6 | owners: string[]; 7 | threshold: number; 8 | chainId: number; 9 | nonce: number; 10 | address: Address; 11 | safeSdk: Safe; 12 | safeSdk2: Safe; 13 | }; 14 | 15 | export interface NetworkContext { 16 | walletProvider: BrowserProvider; 17 | currentNetwork: number; 18 | } 19 | 20 | export interface SafeContext { 21 | safeInformation: SafeInformationType; 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 🛎️ 14 | uses: actions/checkout@master 15 | 16 | - run: corepack enable 17 | - run: pnpm install --frozen-lockfile 18 | 19 | - name: Build 🏗️ 20 | run: pnpm build 21 | 22 | - name: Deploy to GH Pages 🚀 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: dist 27 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs', 'node_modules'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | "@typescript-eslint/no-explicit-any": "off" 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/etherFormatting.ts: -------------------------------------------------------------------------------- 1 | import { formatEther, parseEther } from "viem"; 2 | import { Proposal } from "../schemas/proposal"; 3 | 4 | export function transformValuesToWei(proposal: Proposal): Proposal { 5 | return { 6 | ...proposal, 7 | actions: proposal.actions?.map((action) => ({ 8 | ...action, 9 | value: parseEther(action.value).toString(), 10 | })), 11 | }; 12 | } 13 | 14 | export function transformValuesFromWei(proposal: Proposal): Proposal { 15 | return { 16 | ...proposal, 17 | actions: proposal.actions?.map((action) => ({ 18 | ...action, 19 | value: formatEther(BigInt(action.value)), 20 | })), 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/app/SafeInformationPage.tsx: -------------------------------------------------------------------------------- 1 | import { Button, View } from "reshaped"; 2 | import { SafeInformation } from "../components/SafeInformation"; 3 | import { useNavigate, useParams } from "react-router-dom"; 4 | import { useUpdateProposalInQuery } from "../hooks/useUpdateProposalViaQuery"; 5 | 6 | export const SafeInformationPage = () => { 7 | const { networkId, safeAddress } = useParams(); 8 | const navigate = useNavigate(); 9 | 10 | const onNewProposalClick = () => { 11 | navigate(`/safe/${networkId}/${safeAddress}/new`); 12 | }; 13 | 14 | const { addAction } = useUpdateProposalInQuery({ proposal: undefined }); 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "types": [] 23 | }, 24 | "include": ["src"], 25 | "exclude": [ 26 | "node_modules" 27 | ], 28 | "references": [{ "path": "./tsconfig.node.json" }] 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/get.ts: -------------------------------------------------------------------------------- 1 | // WARNING: This is not a drop in replacement solution and 2 | // it might not work for some edge cases. Test your code! 3 | export const get = (obj: any, path: string, defValue?: any) => { 4 | // If path is not defined or it has false value 5 | if (!path) { 6 | return undefined; 7 | } 8 | // Check if path is string or array. Regex : ensure that we do not have '.' and brackets. 9 | // Regex explained: https://regexr.com/58j0k 10 | const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g); 11 | if (!pathArray) { 12 | return undefined; 13 | } 14 | // Find value 15 | const result = pathArray.reduce( 16 | (prevObj, key) => prevObj && prevObj[key], 17 | obj, 18 | ); 19 | // If found value is undefined return default value; otherwise return the value 20 | return result === undefined ? defValue : result; 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/GenericField.tsx: -------------------------------------------------------------------------------- 1 | import { FormControl, TextField, View } from "reshaped"; 2 | import { get } from "../utils/get"; 3 | 4 | export const GenericField = ({ 5 | label, 6 | fieldProps, 7 | }: { 8 | label: string; 9 | fieldProps?: any; 10 | }) => { 11 | return ({ field: { name, value, onChange }, form: { errors } }: any) => { 12 | const error = get(errors, name); 13 | 14 | return ( 15 | 16 | 17 | {label} 18 | onChange(event)} 22 | {...fieldProps} 23 | /> 24 | {error && {error}} 25 | 26 | 27 | ); 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/hooks/useUpdateProposalViaQuery.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { Proposal } from "../schemas/proposal"; 3 | import { useRedirectToProposalWithNewParams } from "./useSetParamsFromQuery"; 4 | 5 | export type AddAction = ( 6 | newAction: NonNullable[0], 7 | ) => void; 8 | 9 | export type UpdateProposal = { 10 | addAction: AddAction; 11 | replace: (proposal: Proposal) => void; 12 | }; 13 | export const useUpdateProposalInQuery = ({ 14 | proposal, 15 | }: { 16 | proposal: Proposal | undefined; 17 | }): UpdateProposal => { 18 | const setParams = useRedirectToProposalWithNewParams(); 19 | 20 | const addAction = useCallback( 21 | (newAction: NonNullable[0]) => { 22 | setParams({ 23 | actions: [...(proposal?.actions || []), newAction], 24 | nonce: proposal?.nonce, 25 | }); 26 | }, 27 | [proposal, setParams], 28 | ); 29 | 30 | return { 31 | addAction, 32 | replace: setParams, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/schemas/proposal.ts: -------------------------------------------------------------------------------- 1 | import { InferType, array, number, object, string } from "yup"; 2 | import { yupAddress } from "../utils/validators"; 3 | 4 | export const proposalSchema = object({ 5 | nonce: number().nullable(), 6 | actions: array( 7 | object({ 8 | to: yupAddress, 9 | value: string() 10 | .default("0") 11 | .matches( 12 | /^[0-9]+(\.[0-9]+)?$/, 13 | "Needs to be a ETH price (0, 1, or 0.23)", 14 | ) 15 | .required(), 16 | data: string() 17 | .default("0x") 18 | .matches( 19 | /^0x(?:[0-9A-Za-z][0-9A-Za-z])*$/, 20 | "Data is required to match hex format", 21 | ) 22 | .required(), 23 | }), 24 | ), 25 | }); 26 | 27 | export interface Proposal extends InferType { 28 | // using interface instead of type generally gives nicer editor feedback 29 | } 30 | 31 | export const DEFAULT_ACTION_ITEM = { 32 | to: "0x", 33 | value: "0", 34 | data: "0x", 35 | }; 36 | 37 | export const DEFAULT_PROPOSAL = { 38 | nonce: null, 39 | actions: [DEFAULT_ACTION_ITEM], 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/AddressView.tsx: -------------------------------------------------------------------------------- 1 | import { goerli, mainnet, optimism, zora, zoraTestnet } from "viem/chains"; 2 | import { useOutletContext } from "react-router-dom"; 3 | import { NetworkContext } from "./Contexts"; 4 | 5 | export const networkToExplorer = { 6 | [mainnet.id]: "https://etherscan.io/", 7 | [zora.id]: "https://explorer.zora.energy/", 8 | [goerli.id]: "https://goerli.etherscan.io/", 9 | [zoraTestnet.id]: "https://testnet.explorer.zora.energy/", 10 | [optimism.id]: "https://optimistic.etherscan.io/", 11 | }; 12 | 13 | export const AddressView = ({ 14 | address, 15 | prettyName, 16 | }: { 17 | address: `0x${string}`; 18 | prettyName?: string; 19 | }) => { 20 | const currentNetwork = useOutletContext().currentNetwork; 21 | // const { data: ensName } = useEns({ address, enabled: !prettyName }); 22 | const ensName = null; 23 | 24 | return ( 25 | 26 | {prettyName ? prettyName : ensName ? ensName : address}{" "} 27 | 35 | ↗ 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/NetworkSwitcher.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { useCallback, useState } from "react"; 3 | import { FormControl, Select } from "reshaped"; 4 | import { allowedNetworks } from "../chains"; 5 | import { useNavigate } from "react-router-dom"; 6 | 7 | export const NetworkSwitcher = ({ 8 | currentNetwork, 9 | }: { 10 | currentNetwork: string | undefined; 11 | }) => { 12 | const [currentNetworkValue, setCurrentNetworkValue] = 13 | useState(currentNetwork); 14 | const navigate = useNavigate(); 15 | const changeNetwork = useCallback( 16 | (e: { value: string }) => { 17 | const networkId = e.value; 18 | setCurrentNetworkValue(networkId); 19 | 20 | navigate(`/safe/${networkId}`); 21 | }, 22 | [navigate], 23 | ); 24 | 25 | return ( 26 | 27 | Network: 28 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/app/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider, createHashRouter } from "react-router-dom"; 2 | import { Container, Reshaped } from "reshaped"; 3 | import { App } from "./App"; 4 | // import { Error } from "./Error"; 5 | import { ViewSafe } from "./ViewSafe"; 6 | import { Root } from "./Root"; 7 | import { CreateSafe } from "./CreateSafe"; 8 | import { NewSafeProposal } from "./NewSafeProposal"; 9 | import { SafeInformationPage } from "./SafeInformationPage"; 10 | 11 | const router = createHashRouter([ 12 | { 13 | path: "/", 14 | Component: Root, 15 | children: [ 16 | { 17 | path: "/safe/:networkId", 18 | index: true, 19 | Component: App, 20 | }, 21 | { 22 | path: "/safe/:networkId/create", 23 | Component: CreateSafe, 24 | }, 25 | { 26 | path: "/safe/:networkId/:safeAddress", 27 | Component: ViewSafe, 28 | children: [ 29 | { 30 | path: "/safe/:networkId/:safeAddress", 31 | index: true, 32 | Component: SafeInformationPage, 33 | }, 34 | { 35 | path: "/safe/:networkId/:safeAddress/new", 36 | index: true, 37 | Component: NewSafeProposal, 38 | }, 39 | ], 40 | }, 41 | ], 42 | }, 43 | ]); 44 | 45 | export const Wrapper = () => ( 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | -------------------------------------------------------------------------------- /src/components/DataActionPreview.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { Address, Hex } from "viem"; 3 | import { 4 | base, 5 | baseGoerli, 6 | goerli, 7 | mainnet, 8 | optimism, 9 | zora, 10 | zoraTestnet, 11 | } from "viem/chains"; 12 | import { useOutletContext } from "react-router-dom"; 13 | import { NetworkContext } from "./Contexts"; 14 | 15 | const networkToEtherActor: any = { 16 | [mainnet.id]: "mainnet", 17 | [zora.id]: "zora", 18 | [baseGoerli.id]: "base-goerli", 19 | [base.id]: "base", 20 | [goerli.id]: "goerli", 21 | [zoraTestnet.id]: "zora-goerli", 22 | [optimism.id]: "optimism", 23 | }; 24 | 25 | export const DataActionPreview = ({ to, data }: { to: Address; data: Hex }) => { 26 | const currentNetwork = useOutletContext().currentNetwork; 27 | const [responseData, setResponseData] = useState(); 28 | 29 | const fetchData = useCallback(async () => { 30 | let json; 31 | try { 32 | const response = await fetch( 33 | `https://${networkToEtherActor[Number(currentNetwork)]}.ether.actor/decode/${to}/${data}`, 34 | ); 35 | if (!response.ok) { 36 | throw new Error(); 37 | } 38 | json = await response.json(); 39 | } catch (err: any) { 40 | const response = await fetch(`https://ether.actor/decode/${data}`); 41 | json = await response.json(); 42 | } 43 | setResponseData(json); 44 | }, [to, data, setResponseData]); 45 | 46 | useEffect(() => { 47 | fetchData(); 48 | }, [to, data]); 49 | 50 | return
{JSON.stringify(responseData, null, 2)}
; 51 | }; 52 | -------------------------------------------------------------------------------- /src/hooks/useSetParamsFromQuery.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { 3 | useLocation, 4 | useNavigate, 5 | useParams, 6 | useSearchParams, 7 | } from "react-router-dom"; 8 | import { Proposal } from "../schemas/proposal"; 9 | import { queryKeys } from "./useLoadProposalFromQuery"; 10 | 11 | export const useRedirectToProposalWithNewParams = () => { 12 | const [, setParams] = useSearchParams(); 13 | const { networkId, safeAddress } = useParams(); 14 | const navigate = useNavigate(); 15 | const { pathname } = useLocation(); 16 | 17 | return useCallback( 18 | (proposal: Proposal) => { 19 | if (!proposal.actions?.length) { 20 | return; 21 | } 22 | const params = { 23 | [queryKeys.targets]: proposal 24 | .actions!.map((action) => action.to) 25 | .join("|"), 26 | [queryKeys.calldatas]: proposal 27 | .actions!.map((action) => action.data) 28 | .join("|"), 29 | [queryKeys.values]: proposal 30 | .actions!.map((action) => action.value) 31 | .join("|"), 32 | ...(proposal.nonce 33 | ? { 34 | [queryKeys.nonce]: proposal.nonce.toString(), 35 | } 36 | : {}), 37 | }; 38 | // if we are not in a new safe proposal, we need to redirect to new safe proposal 39 | // so that it shows up in the url, and renders correctly 40 | if (!pathname.includes("new")) { 41 | const newPath = `/safe/${networkId}/${safeAddress}/new`; 42 | 43 | navigate(newPath, { 44 | replace: true, 45 | }); 46 | } 47 | setParams(params); 48 | }, 49 | [setParams, networkId, safeAddress, pathname, navigate], 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/hooks/useLoadProposalFromQuery.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useSearchParams } from "react-router-dom"; 3 | import { Proposal } from "../schemas/proposal"; 4 | 5 | export const queryKeys = { 6 | targets: "targets", 7 | calldatas: "calldatas", 8 | values: "values", 9 | nonce: "nonce", 10 | } as const; 11 | 12 | export const useLoadProposalFromQuery = () => { 13 | const [proposal, setProposal] = useState(); 14 | const [params] = useSearchParams(); 15 | 16 | useEffect(() => { 17 | const targets = params.get(queryKeys["targets"])?.split("|"); 18 | const calldatas = params.get(queryKeys["calldatas"])?.split("|"); 19 | const values = params.get(queryKeys["values"])?.split("|"); 20 | const nonce = params.get(queryKeys["nonce"]); 21 | 22 | if (targets && calldatas) { 23 | // ensure the 3 lengths are the same. check if values also have the same length if it's not empty 24 | // check the inverse of the above, if inverse is true, return: 25 | if ( 26 | targets.length !== calldatas.length || 27 | (values?.length && values?.length !== targets.length) 28 | ) { 29 | console.log("invalid lengths"); 30 | return; 31 | } 32 | 33 | const actions = targets.map((target, index) => ({ 34 | to: target, 35 | data: calldatas[index]!, 36 | value: (values && values[index]) || "0", 37 | })); 38 | 39 | console.log({ actions, txt: "setting proposal" }); 40 | 41 | const proposal: Proposal = { 42 | actions, 43 | ...(nonce ? { nonce: parseInt(nonce) } : {}), 44 | }; 45 | 46 | setProposal(proposal); 47 | } 48 | }, [params, setProposal]); 49 | 50 | return proposal; 51 | }; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smol-safe", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "type": "module", 6 | "dependencies": { 7 | "@safe-global/protocol-kit": "^6.1.1", 8 | "@safe-global/safe-deployments": "^1.37.14", 9 | "@zoralabs/zorb": "^0.1.0", 10 | "buffer": "^6.0.3", 11 | "ethers": "^6.7.1", 12 | "formik": "^2.4.5", 13 | "lodash": "^4.17.21", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-feather": "^2.0.10", 17 | "react-router-dom": "^6.20.0", 18 | "reshaped": "^2.6.0", 19 | "toastify-js": "^1.12.0", 20 | "viem": "^2.38.2", 21 | "yup": "^1.3.2" 22 | }, 23 | "scripts": { 24 | "dev": "vite", 25 | "build": "tsc && vite build --base=/smol-safe/", 26 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 27 | "preview": "vite preview", 28 | "prettier": "prettier src/ --write", 29 | "deploy": "pnpm build && gh-pages -d dist" 30 | }, 31 | "devDependencies": { 32 | "@safe-global/safe-core-sdk-types": "^5.1.0", 33 | "@types/react": "^18.2.38", 34 | "@types/react-dom": "^18.2.16", 35 | "@typescript-eslint/eslint-plugin": "^6.12.0", 36 | "@typescript-eslint/parser": "^6.12.0", 37 | "@vitejs/plugin-react-swc": "^3.5.0", 38 | "assert": "^2.0.0", 39 | "eslint": "^8.54.0", 40 | "eslint-plugin-react-hooks": "^4.6.0", 41 | "eslint-plugin-react-refresh": "^0.4.4", 42 | "events": "^3.1.0", 43 | "gh-pages": "^5.0.0", 44 | "os-browserify": "^0.3.0", 45 | "parcel": "^2.9.2", 46 | "path-browserify": "^1.0.0", 47 | "prettier": "^3.2.4", 48 | "stream-browserify": "^3.0.0", 49 | "ts-node": "^10.9.1", 50 | "typescript": "^5.3.2", 51 | "vite": "^5.0.2" 52 | }, 53 | "packageManager": "pnpm@10.19.0+sha512.c9fc7236e92adf5c8af42fd5bf1612df99c2ceb62f27047032f4720b33f8eacdde311865e91c411f2774f618d82f320808ecb51718bfa82c060c4ba7c76a32b8" 54 | } 55 | -------------------------------------------------------------------------------- /src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import { FormControl, Button, TextField, Divider, View, Card } from "reshaped"; 2 | import { useFormik } from "formik"; 3 | import { isAddress } from "viem"; 4 | import { useNavigate, useOutletContext } from "react-router-dom"; 5 | import { NetworkContext } from "../components/Contexts"; 6 | 7 | function FormAddressInput() { 8 | const navigate = useNavigate(); 9 | 10 | const { currentNetwork: network } = useOutletContext(); 11 | 12 | const formik = useFormik({ 13 | initialValues: { 14 | address: "", 15 | }, 16 | validate: (data) => { 17 | const errors = {}; 18 | if (!isAddress(data.address)) { 19 | return { address: "Invalid Address" }; 20 | } 21 | return errors; 22 | }, 23 | onSubmit: (values) => { 24 | navigate(`/safe/${network}/${values.address}`); 25 | }, 26 | }); 27 | 28 | return ( 29 |
30 | 31 | Safe Address: 32 | formik.handleChange(event)} 35 | onBlur={formik.handleBlur} 36 | name="address" 37 | /> 38 | {formik.errors.address && ( 39 | {formik.errors.address} 40 | )} 41 | 42 | Remember to select the correct network 43 | 44 | 45 | 46 | 47 | 48 |
49 | ); 50 | } 51 | 52 | export function App() { 53 | const navigate = useNavigate(); 54 | 55 | const chainId = useOutletContext().currentNetwork; 56 | 57 | return ( 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 73 | 74 | 75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/chains.ts: -------------------------------------------------------------------------------- 1 | import { ContractNetworksConfig, ContractNetworkConfig } from "@safe-global/protocol-kit"; 2 | import * as chains from "viem/chains"; 3 | 4 | const defaultL2Addresses: ContractNetworkConfig = { 5 | multiSendAddress: "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761", 6 | safeProxyFactoryAddress: "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", 7 | multiSendCallOnlyAddress: "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", 8 | fallbackHandlerAddress: "0x1AC114C2099aFAf5261731655Dc6c306bFcd4Dbd", 9 | createCallAddress: "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4", 10 | signMessageLibAddress: "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2", 11 | // renamed from safeMasterCopyAddress 12 | safeSingletonAddress: "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", 13 | simulateTxAccessorAddress: "0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da", 14 | }; 15 | 16 | const zkAddresses: ContractNetworkConfig = { 17 | multiSendAddress: "0x0dFcccB95225ffB03c6FBB2559B530C2B7C8A912", 18 | safeProxyFactoryAddress: "0xDAec33641865E4651fB43181C6DB6f7232Ee91c2", 19 | multiSendCallOnlyAddress: "0xf220D3b4DFb23C4ade8C88E526C1353AbAcbC38F", 20 | fallbackHandlerAddress: "0x2f870a80647BbC554F3a0EBD093f11B4d2a7492A", 21 | createCallAddress: "0xcB8e5E438c5c2b45FbE17B02Ca9aF91509a8ad56", 22 | signMessageLibAddress: "0x357147caf9C0cCa67DfA0CF5369318d8193c8407", 23 | // renamed from safeMasterCopyAddress 24 | safeSingletonAddress: "0x1727c2c531cf966f902E5927b98490fDFb3b2b70", 25 | simulateTxAccessorAddress: "0x4191E2e12E8BC5002424CE0c51f9947b02675a44", 26 | }; 27 | 28 | // Example how to add new networks before they are merged and released from `safe-global/safe-deployments` package. 29 | export const contractNetworks: ContractNetworksConfig = { 30 | [`${chains.abstractTestnet.id}`]: zkAddresses, 31 | [`${chains.abstract.id}`]: zkAddresses, 32 | [`${chains.zoraSepolia.id}`]: defaultL2Addresses, 33 | [`${chains.blastSepolia.id}`]: defaultL2Addresses, 34 | [`${chains.optimismSepolia.id}`]: defaultL2Addresses, 35 | [`${chains.blast.id}`]: defaultL2Addresses, 36 | [`${chains.unichainSepolia.id}`]: defaultL2Addresses, 37 | [`${chains.zksyncSepoliaTestnet.id}`]: zkAddresses, 38 | [`${chains.zksync.id}`]: zkAddresses, 39 | }; 40 | 41 | export const allowedNetworks: { [chainId: number]: chains.Chain } = { 42 | [chains.mainnet.id]: chains.mainnet, 43 | [chains.zora.id]: chains.zora, 44 | [chains.zoraTestnet.id]: chains.zoraTestnet, 45 | [chains.zoraSepolia.id]: chains.zoraSepolia, 46 | [chains.arbitrumGoerli.id]: chains.arbitrumGoerli, 47 | [chains.arbitrumSepolia.id]: chains.arbitrumSepolia, 48 | [chains.arbitrumNova.id]: chains.arbitrumNova, 49 | [chains.arbitrum.id]: chains.arbitrum, 50 | [chains.base.id]: chains.base, 51 | [chains.baseSepolia.id]: chains.baseSepolia, 52 | [chains.sepolia.id]: chains.sepolia, 53 | [chains.optimism.id]: chains.optimism, 54 | [chains.optimismSepolia.id]: chains.optimismSepolia, 55 | [chains.blastSepolia.id]: chains.blastSepolia, 56 | [chains.blast.id]: chains.blast, 57 | [chains.zksync.id]: chains.zkSync, 58 | [chains.zksyncSepoliaTestnet.id]: chains.zksyncSepoliaTestnet, 59 | [chains.unichainSepolia.id]: chains.unichainSepolia, 60 | [chains.abstractTestnet.id]: chains.abstractTestnet, 61 | [chains.abstract.id]: chains.abstract, 62 | }; 63 | 64 | Object.keys(contractNetworks).map((network) => { 65 | if (allowedNetworks[+network]) { 66 | // if already exists skip 67 | return; 68 | } 69 | const viemChain = Object.values(chains).find( 70 | (chain) => chain.id.toString() === network 71 | ); 72 | 73 | if (!viemChain) { 74 | return; 75 | } 76 | allowedNetworks[+network] = viemChain; 77 | }); 78 | -------------------------------------------------------------------------------- /src/app/ViewSafe.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback, useMemo } from "react"; 2 | import { Outlet, useOutletContext, useParams } from "react-router-dom"; 3 | import { Signer } from "ethers"; 4 | import Safe from "@safe-global/protocol-kit"; 5 | import { contractNetworks } from "../chains"; 6 | import { Button, View, Text } from "reshaped"; 7 | import { Address } from "viem"; 8 | import { 9 | NetworkContext, 10 | SafeContext, 11 | SafeInformationType, 12 | } from "../components/Contexts"; 13 | 14 | type SafeData = Awaited>; 15 | 16 | async function getSafeSDK(safeAddress: string, signer: Signer) { 17 | const signerAddress = await signer.getAddress(); 18 | 19 | const safeSdk: Safe = await Safe.init({ 20 | provider: (window as any).ethereum, 21 | signer: signerAddress, 22 | safeAddress, 23 | contractNetworks, 24 | }); 25 | 26 | const safeSdk2 = await Safe.init({ 27 | provider: (window as any).ethereum, 28 | signer: signerAddress, 29 | safeAddress, 30 | contractNetworks, 31 | }); 32 | 33 | return { safeSdk, safeSdk2, signer }; 34 | } 35 | 36 | const useLoadSafeInformation = ({ 37 | safeData, 38 | }: { 39 | safeData: SafeData | undefined; 40 | }) => { 41 | const [safeInformation, setSafeInformation] = useState(); 42 | useEffect(() => { 43 | if (!safeData) return; 44 | 45 | const loadSafeInfo = async () => { 46 | const { safeSdk, safeSdk2 } = safeData; 47 | const owners = await safeSdk.getOwners(); 48 | const threshold = await safeSdk.getThreshold(); 49 | const chainId = Number(await safeSdk.getChainId()); 50 | const nonce = await safeSdk.getNonce(); 51 | const address = (await safeSdk.getAddress()) as Address; 52 | 53 | setSafeInformation({ 54 | owners, 55 | threshold, 56 | chainId, 57 | nonce, 58 | address, 59 | safeSdk, 60 | safeSdk2, 61 | }); 62 | }; 63 | 64 | loadSafeInfo(); 65 | }, [safeData, setSafeInformation]); 66 | 67 | return safeInformation; 68 | }; 69 | 70 | export const ViewSafe = () => { 71 | const params = useParams(); 72 | const [safeData, setSafeData] = useState(); 73 | const { walletProvider: providerContext } = 74 | useOutletContext(); 75 | 76 | const setupSafe = useCallback(async () => { 77 | if (params.safeAddress && providerContext) { 78 | setSafeData( 79 | await getSafeSDK(params.safeAddress, await providerContext.getSigner()), 80 | ); 81 | } 82 | }, [params.safeAddress, providerContext]); 83 | 84 | const switchNetwork = useCallback(() => { 85 | if (!params.networkId) return; 86 | providerContext?.send("wallet_switchEthereumChain", [ 87 | { 88 | chainId: `0x${parseInt(params.networkId!).toString(16)}`, 89 | }, 90 | ]); 91 | }, [params.networkId, providerContext]); 92 | 93 | useEffect(() => { 94 | switchNetwork(); 95 | }, [switchNetwork]); 96 | 97 | useEffect(() => { 98 | setupSafe(); 99 | }, [setupSafe]); 100 | 101 | const safeInformation = useLoadSafeInformation({ safeData }); 102 | 103 | const safeInformationContext: SafeContext | undefined = useMemo(() => { 104 | if (!safeInformation) return; 105 | return { 106 | safeInformation, 107 | }; 108 | }, [safeInformation]); 109 | 110 | return ( 111 | 112 | View Safe 113 | 114 | {safeInformationContext ? ( 115 | 116 | ) : ( 117 | 118 | )} 119 | 120 | ); 121 | }; 122 | -------------------------------------------------------------------------------- /src/components/SafeInformation.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Button, Card, Text, View } from "reshaped"; 3 | import { allowedNetworks } from "../chains"; 4 | import { InfoBox } from "../components/InfoBox"; 5 | import { Address } from "viem"; 6 | import { AddressView } from "../components/AddressView"; 7 | import { OwnerAction, SetOwnerModal } from "../components/SetOwnerModal"; 8 | import { useOutletContext } from "react-router-dom"; 9 | import { SafeContext } from "./Contexts"; 10 | import { AddAction } from "../hooks/useUpdateProposalViaQuery"; 11 | 12 | const SafeInformationItem = ({ 13 | title, 14 | description, 15 | children, 16 | }: { 17 | title: string; 18 | description: string; 19 | children: React.ReactNode; 20 | }) => ( 21 | <> 22 | 23 | {title}: {description} 24 | 25 | {children} 26 | 27 | ); 28 | 29 | export const SafeInformation = ({ 30 | children, 31 | addAction, 32 | }: { 33 | children?: React.ReactNode; 34 | addAction: AddAction; 35 | }) => { 36 | const [ownerAction, setOwnerAction] = useState(); 37 | 38 | const { safeInformation } = useOutletContext(); 39 | 40 | return ( 41 |
42 | {ownerAction && ( 43 | { 45 | setOwnerAction(undefined); 46 | }} 47 | action={ownerAction} 48 | addAction={addAction} 49 | /> 50 | )} 51 | 52 | 53 | 54 | 58 | {allowedNetworks[safeInformation.chainId]?.name || 59 | safeInformation.chainId.toString()} 60 | 61 | 62 | 63 | 68 | {safeInformation.threshold} 69 | 70 | 71 | 72 | 76 | 77 | {safeInformation.owners.map((owner) => ( 78 | 79 | 80 | 81 | 90 | 91 | 92 | ))} 93 | 94 | 95 | 96 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 114 | {safeInformation.nonce} 115 | 116 | 117 | 118 | 119 | {children} 120 |
121 | ); 122 | }; 123 | -------------------------------------------------------------------------------- /src/app/Root.tsx: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { useCallback, useEffect, useMemo, useState } from "react"; 3 | import { Outlet, useParams } from "react-router-dom"; 4 | import { Button, View } from "reshaped"; 5 | import { NetworkSwitcher } from "../components/NetworkSwitcher"; 6 | import { BrowserProvider } from "ethers"; 7 | import { NetworkContext } from "../components/Contexts"; 8 | import { allowedNetworks } from "../chains"; 9 | 10 | export const Root = () => { 11 | const [provider, setProvider] = useState< 12 | ethers.BrowserProvider | undefined 13 | >(); 14 | const networkIdFromRoute = useParams().networkId; 15 | 16 | const [currentNetwork, setCurrentNetwork] = useState(0); 17 | const [multichainSession, setMultichainSession] = useState(false); 18 | 19 | const connectMetamask = useCallback(async () => { 20 | const provider = new BrowserProvider((window as any).ethereum, "any"); 21 | 22 | // Check if MetaMask supports multichain API 23 | const isMultichainSupported = typeof (window as any).ethereum?.request === 'function'; 24 | 25 | if (isMultichainSupported) { 26 | try { 27 | // Create a multichain session with all allowed networks 28 | const optionalScopes = Object.keys(allowedNetworks).reduce((acc, chainId) => { 29 | acc[`eip155:${chainId}`] = { 30 | methods: [ 31 | "eth_sendTransaction", 32 | "eth_signTransaction", 33 | "eth_sign", 34 | "personal_sign", 35 | "eth_signTypedData", 36 | "eth_signTypedData_v4", 37 | "eth_getBalance", 38 | "eth_call" 39 | ], 40 | accounts: [] 41 | }; 42 | return acc; 43 | }, {} as Record); 44 | 45 | await (window as any).ethereum.request({ 46 | method: "wallet_createSession", 47 | params: { 48 | optionalScopes 49 | } 50 | }); 51 | 52 | setMultichainSession(true); 53 | console.log("Multichain session created"); 54 | } catch (error) { 55 | console.log("Multichain API not available, falling back to standard connection", error); 56 | } 57 | } 58 | 59 | provider.on("accountsChanged", async (accounts) => { 60 | console.log({ accounts }); 61 | const newNetwork = await provider.getNetwork(); 62 | setCurrentNetwork(Number(newNetwork.chainId)); 63 | }); 64 | provider.on("disconnect", () => { 65 | setProvider(undefined); 66 | setCurrentNetwork(0); 67 | setMultichainSession(false); 68 | }); 69 | provider.on("connect", async () => { 70 | setProvider(provider); 71 | const network = await provider.getNetwork(); 72 | setCurrentNetwork(Number(network.chainId)); 73 | }); 74 | await provider.send("eth_requestAccounts", []); 75 | const signer = await provider.getSigner(); 76 | if (provider && signer) { 77 | const network = await provider.getNetwork(); 78 | setCurrentNetwork(Number(network.chainId)); 79 | setProvider(provider); 80 | } 81 | }, [setProvider, setCurrentNetwork]); 82 | useEffect(() => { 83 | connectMetamask(); 84 | }, [connectMetamask]); 85 | 86 | const networkContext: NetworkContext | undefined = useMemo(() => { 87 | if (!provider) return; 88 | 89 | return { 90 | walletProvider: provider, 91 | currentNetwork, 92 | }; 93 | }, [provider, currentNetwork]); 94 | 95 | useEffect(() => { 96 | if (!networkIdFromRoute) return; 97 | if (currentNetwork !== Number(networkIdFromRoute)) { 98 | provider?.send("wallet_switchEthereumChain", [ 99 | { 100 | chainId: `0x${parseInt(networkIdFromRoute).toString(16)}`, 101 | }, 102 | ]); 103 | 104 | setCurrentNetwork(Number(networkIdFromRoute)); 105 | } 106 | }, [currentNetwork, networkIdFromRoute, setCurrentNetwork, provider]); 107 | 108 | if (!networkContext) { 109 | return ( 110 | 111 | 112 | 113 | ); 114 | } 115 | 116 | return ( 117 | <> 118 |
119 |
120 | 121 | 122 | 123 | ); 124 | }; 125 | -------------------------------------------------------------------------------- /src/components/SetOwnerModal.tsx: -------------------------------------------------------------------------------- 1 | import { SyntheticEvent } from "react"; 2 | import { Button, Modal, Text, View, useToast } from "reshaped"; 3 | import { AddressView } from "./AddressView"; 4 | import { Address } from "viem"; 5 | import { Field, Form, Formik } from "formik"; 6 | import { GenericField } from "./GenericField"; 7 | import { yupAddress } from "../utils/validators"; 8 | import { number, object } from "yup"; 9 | import { useOutletContext } from "react-router-dom"; 10 | import { SafeContext } from "./Contexts"; 11 | import { AddAction } from "../hooks/useUpdateProposalViaQuery"; 12 | 13 | export type OwnerAction = 14 | | undefined 15 | | { 16 | type: "add"; 17 | } 18 | | { 19 | type: "remove"; 20 | address: string; 21 | }; 22 | 23 | const ButtonPanel = ({ 24 | onClick, 25 | actionDisabled, 26 | onClose, 27 | }: { 28 | onClose: () => void; 29 | onClick?: (button: SyntheticEvent) => void; 30 | actionDisabled?: boolean; 31 | }) => ( 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | 44 | const AddOwnerModalContent = ({ 45 | onClose, 46 | addAction, 47 | }: { 48 | onClose: () => void; 49 | addAction: AddAction; 50 | }) => { 51 | const { safeInformation } = useOutletContext(); 52 | const toast = useToast(); 53 | 54 | return ( 55 | { 59 | if (!safeInformation) { 60 | return; 61 | } 62 | 63 | try { 64 | const addOwnerTx = await safeInformation.safeSdk.createAddOwnerTx({ 65 | ownerAddress: address, 66 | threshold: threshold, 67 | }); 68 | addAction({ 69 | data: addOwnerTx.data.data, 70 | value: "0", 71 | to: safeInformation.address, 72 | }); 73 | } catch (err: any) { 74 | toast.show({ title: "Error Updating Safe", text: err.toString() }); 75 | } 76 | onClose(); 77 | }} 78 | > 79 |
80 | Add Signer 81 | 82 | {GenericField({ label: "New User Address" })} 83 | 84 | 85 | 86 | {GenericField({ 87 | label: "Threshold", 88 | fieldProps: { type: "number" }, 89 | })} 90 | 91 | 92 | 93 | 94 | 95 |
96 | ); 97 | }; 98 | 99 | const RemoveOwnerModalContent = ({ 100 | onClose, 101 | target, 102 | addAction, 103 | }: { 104 | onClose: () => void; 105 | target: string; 106 | addAction: AddAction; 107 | }) => { 108 | const { safeInformation } = useOutletContext(); 109 | const toaster = useToast(); 110 | 111 | const onSubmitClick = async ({ threshold }: any) => { 112 | try { 113 | const removeOwnerTx = await safeInformation?.safeSdk2.createRemoveOwnerTx( 114 | { 115 | ownerAddress: target, 116 | threshold: threshold, 117 | }, 118 | ); 119 | if (!removeOwnerTx || !safeInformation) { 120 | return; 121 | } 122 | 123 | addAction({ 124 | data: removeOwnerTx.data.data, 125 | value: "0", 126 | to: safeInformation.address, 127 | }); 128 | onClose(); 129 | } catch (err: any) { 130 | toaster.show({ 131 | title: "Error Removing Owner", 132 | text: `Error setting up transaction: ${err.toString()}`, 133 | }); 134 | } 135 | }; 136 | return ( 137 | 142 |
143 | Remove Owner 144 | 145 | Owner: 146 | 147 | 148 | 149 | {GenericField({ 150 | label: "Threshold", 151 | fieldProps: { type: "number" }, 152 | })} 153 | 154 | 155 | 156 | 157 | 158 |
159 | ); 160 | }; 161 | 162 | export const SetOwnerModal = ({ 163 | action, 164 | onClose, 165 | addAction, 166 | }: { 167 | action: OwnerAction; 168 | onClose: () => void; 169 | addAction: AddAction; 170 | }) => { 171 | return ( 172 | 173 | {action?.type === "remove" && ( 174 | 179 | )} 180 | {action?.type === "add" && ( 181 | 182 | )} 183 | 184 | ); 185 | }; 186 | -------------------------------------------------------------------------------- /src/app/CreateSafe.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { Field, FieldArray, Formik } from "formik"; 3 | import { Text, Button, FormControl, TextField, View, useToast } from "reshaped"; 4 | import { allowedNetworks, contractNetworks } from "../chains"; 5 | import { isAddress } from "viem"; 6 | import Safe, { SafeAccountConfig, SafeDeploymentConfig, SafeProvider } from "@safe-global/protocol-kit"; 7 | import { useNavigate, useOutletContext } from "react-router-dom"; 8 | import { AbstractSigner } from "ethers"; 9 | import { BrowserProvider } from "ethers"; 10 | import { NetworkContext } from "../components/Contexts"; 11 | 12 | function validateAddress(value: string) { 13 | if (!isAddress(value)) { 14 | return "Invalid address"; 15 | } 16 | } 17 | 18 | function validateSafeArguments(values: any) { 19 | const errors: any = {}; 20 | 21 | if (values.threshold <= 0) { 22 | errors.threshold = "Threshold needs to be at least 1"; 23 | } 24 | if (values.threshold > values.addresses.length) { 25 | errors.threshold = "Threshold cannot be more than the number of addresses"; 26 | } 27 | 28 | return errors; 29 | } 30 | 31 | export function CreateSafe() { 32 | const { walletProvider: provider } = useOutletContext(); 33 | const { currentNetwork: network } = useOutletContext(); 34 | const toaster = useToast(); 35 | const navigate = useNavigate(); 36 | const [signerInfo, setSignerInfo] = useState< 37 | undefined | { signer: AbstractSigner; address: string } 38 | >(undefined); 39 | 40 | const updateSignerInfo = useCallback( 41 | async (provider: BrowserProvider) => { 42 | const signer = await provider.getSigner(); 43 | const address = await signer.getAddress(); 44 | setSignerInfo({ signer, address }); 45 | }, 46 | [setSignerInfo], 47 | ); 48 | 49 | useEffect(() => { 50 | if (provider) { 51 | updateSignerInfo(provider); 52 | } 53 | }, [provider, updateSignerInfo]); 54 | 55 | const submitCallback = useCallback( 56 | async (data: any) => { 57 | if (!signerInfo) { 58 | return; 59 | } 60 | try { 61 | const safeAccountConfig: SafeAccountConfig = { 62 | owners: data.addresses, 63 | threshold: parseInt(data.threshold, 10), 64 | }; 65 | 66 | const safeDeploymentConfig: SafeDeploymentConfig = { 67 | saltNonce: Date.now().toString(), 68 | }; 69 | 70 | // Create SafeProvider 71 | const safeProvider = new SafeProvider({ 72 | provider: (window as any).ethereum, 73 | signer: await signerInfo.signer.getAddress(), 74 | }); 75 | 76 | // Create and deploy the Safe 77 | const safeSdk = await Safe.init({ 78 | provider: (window as any).ethereum, 79 | signer: await signerInfo.signer.getAddress(), 80 | predictedSafe: { 81 | safeAccountConfig, 82 | safeDeploymentConfig, 83 | }, 84 | contractNetworks, 85 | }); 86 | 87 | // Deploy the Safe 88 | const deploymentTransaction = await safeSdk.createSafeDeploymentTransaction(); 89 | const txResponse = await signerInfo.signer.sendTransaction({ 90 | to: deploymentTransaction.to, 91 | data: deploymentTransaction.data, 92 | value: deploymentTransaction.value, 93 | }); 94 | 95 | await txResponse.wait(); 96 | 97 | const newAddress = await safeSdk.getAddress(); 98 | toaster.show({ 99 | title: "Created a new safe!", 100 | text: `Opening safe... The new safe address is ${newAddress}`, 101 | }); 102 | navigate(`/safe/${network}/${newAddress}`); 103 | } catch (e: any) { 104 | toaster.show({ 105 | title: "Error creating safe", 106 | text: `Message: ${e.message}`, 107 | }); 108 | } 109 | }, 110 | [navigate, network, signerInfo, toaster], 111 | ); 112 | 113 | return ( 114 | 115 | Create a new safe 116 | 117 | Network: {allowedNetworks[Number(network)]?.name || "unknown"} 118 | 119 | 127 | {({ handleSubmit, handleChange, isSubmitting, values, errors }) => ( 128 |
129 | ( 132 | <> 133 | {values.addresses.map((address, indx) => ( 134 | 135 | 139 | {({ field: { name, value } }: any) => ( 140 | 146 | 147 | Signer address {indx + 1}:{" "} 148 | 149 | handleChange(event)} 153 | endSlot={ 154 | 161 | } 162 | /> 163 | {errors.addresses && errors.addresses[indx] && ( 164 | 165 | {errors.addresses[indx].toString()} 166 | 167 | )} 168 | 169 | )} 170 | 171 | 172 | ))} 173 | 174 | 175 | 176 | 177 | 178 | 179 | )} 180 | /> 181 | 182 | Threshold: 183 | handleChange(event)} 188 | /> 189 | {errors.threshold && ( 190 | {errors.threshold} 191 | )} 192 | 193 | This number of signers needs to approve 194 | 195 | 196 | 197 | 209 | 210 | 211 | )} 212 |
213 |
214 | ); 215 | } 216 | -------------------------------------------------------------------------------- /src/app/NewSafeProposal.tsx: -------------------------------------------------------------------------------- 1 | import { Field, FieldArray, Formik } from "formik"; 2 | import { SafeInformation } from "../components/SafeInformation"; 3 | import { Card, View, Text, Button, useToast } from "reshaped"; 4 | import { SyntheticEvent, useCallback, useEffect, useState } from "react"; 5 | import { Address, Hex, formatEther } from "viem"; 6 | import { validateAddress, validateETH } from "../utils/validators"; 7 | import { GenericField } from "../components/GenericField"; 8 | import { DataActionPreview } from "../components/DataActionPreview"; 9 | import Safe from "@safe-global/protocol-kit"; 10 | import { 11 | DEFAULT_ACTION_ITEM, 12 | DEFAULT_PROPOSAL, 13 | Proposal, 14 | proposalSchema, 15 | } from "../schemas/proposal"; 16 | import { useLoadProposalFromQuery } from "../hooks/useLoadProposalFromQuery"; 17 | import { 18 | transformValuesFromWei, 19 | transformValuesToWei, 20 | } from "../utils/etherFormatting"; 21 | import { useOutletContext } from "react-router-dom"; 22 | import { NetworkContext, SafeContext } from "../components/Contexts"; 23 | import { useUpdateProposalInQuery } from "../hooks/useUpdateProposalViaQuery"; 24 | 25 | const FormActionItem = ({ 26 | name, 27 | indx, 28 | remove, 29 | }: { 30 | name: string; 31 | indx: number; 32 | remove: (indx: number) => void; 33 | }) => { 34 | return ( 35 | 36 | 37 | Action #{indx} 38 | 41 | 42 | 43 | {GenericField({ label: "Destination Contract" })} 44 | 45 | 46 | {GenericField({ 47 | label: "Value (in ETH)", 48 | fieldProps: { type: "number" }, 49 | })} 50 | 51 | {GenericField({ label: "Data" })} 52 | 53 | ); 54 | }; 55 | 56 | const createSafeTransaction = async ({ 57 | proposal, 58 | safe, 59 | }: { 60 | proposal: Proposal; 61 | safe: Safe; 62 | }) => { 63 | if (!proposal.actions) { 64 | return; 65 | } 66 | 67 | return await safe.createTransaction({ 68 | transactions: proposal.actions, 69 | options: { nonce: proposal.nonce || undefined }, 70 | }); 71 | }; 72 | 73 | const signTx = async ({ 74 | proposal, 75 | safe, 76 | }: { 77 | proposal: Proposal; 78 | safe: Safe; 79 | }) => { 80 | const txn = await createSafeTransaction({ 81 | proposal, 82 | safe, 83 | }); 84 | if (!txn) { 85 | throw new Error("No txn"); 86 | } 87 | const txHash = await safe.getTransactionHash(txn); 88 | const executedTxn = await safe.approveTransactionHash(txHash); 89 | // In v6, the transaction is already executed when approveTransactionHash returns 90 | 91 | return executedTxn; 92 | }; 93 | 94 | const signAndExecuteTx = async ({ 95 | proposal, 96 | safe, 97 | }: { 98 | proposal: Proposal; 99 | safe: Safe; 100 | }) => { 101 | const txn = await createSafeTransaction({ 102 | proposal, 103 | safe, 104 | }); 105 | if (!txn) { 106 | throw new Error("No txn"); 107 | } 108 | const executedTxn = await safe.executeTransaction(txn); 109 | // In v6, the transaction is already executed when executeTransaction returns 110 | 111 | return executedTxn; 112 | }; 113 | 114 | const useGetSafeTxApprovals = ({ proposal }: { proposal: Proposal }) => { 115 | const { safeInformation } = useOutletContext(); 116 | 117 | const safeSdk = safeInformation.safeSdk; 118 | const safeSdk2 = safeInformation.safeSdk2; 119 | 120 | const [approvers, setApprovers] = useState([]); 121 | 122 | const loadApprovers = useCallback(async () => { 123 | if (!safeSdk || !safeSdk2) return; 124 | const txn = await createSafeTransaction({ 125 | proposal, 126 | safe: safeSdk, 127 | }); 128 | 129 | if (!txn) return; 130 | 131 | const txHash = await safeSdk.getTransactionHash(txn); 132 | const ownersWhoApprovedTx = await safeSdk2.getOwnersWhoApprovedTx(txHash); 133 | 134 | setApprovers(ownersWhoApprovedTx as Address[]); 135 | }, [proposal, safeSdk, safeSdk2]); 136 | 137 | useEffect(() => { 138 | loadApprovers(); 139 | }, [loadApprovers]); 140 | 141 | return { approvers, loadApprovers }; 142 | }; 143 | 144 | const useAccountAddress = () => { 145 | const { walletProvider } = useOutletContext(); 146 | 147 | const [address, setAddress] = useState
(); 148 | 149 | useEffect(() => { 150 | if (!walletProvider) return; 151 | 152 | (async () => { 153 | const signer = await walletProvider.getSigner(); 154 | setAddress((await signer.getAddress()) as Address | undefined); 155 | })(); 156 | }, [walletProvider]); 157 | 158 | return address; 159 | }; 160 | 161 | function determineIfCanExecute({ 162 | hasApproved, 163 | totalApprovers, 164 | threshold, 165 | }: { 166 | hasApproved: boolean; 167 | totalApprovers: number; 168 | threshold: number; 169 | }) { 170 | const remainingNeeded = threshold - totalApprovers; 171 | 172 | if (remainingNeeded === 0) { 173 | return true; 174 | } 175 | 176 | // if there is one left, and i haven't signed, i can sign and approve 177 | if (remainingNeeded === 1) { 178 | if (!hasApproved) { 179 | return true; 180 | } 181 | } 182 | 183 | return false; 184 | } 185 | 186 | const ViewProposal = ({ 187 | handleEditClicked, 188 | proposal, 189 | }: { 190 | proposal: Proposal; 191 | handleEditClicked: (evt: SyntheticEvent) => void; 192 | }) => { 193 | const { safeInformation } = useOutletContext(); 194 | const safe = safeInformation.safeSdk; 195 | 196 | const toaster = useToast(); 197 | 198 | const { approvers, loadApprovers } = useGetSafeTxApprovals({ proposal }); 199 | 200 | const address = useAccountAddress(); 201 | 202 | const signCallback = useCallback(async () => { 203 | if (!safe) return; 204 | try { 205 | const executedTxn = await signTx({ 206 | proposal: proposal!, 207 | safe, 208 | }); 209 | // call submit 210 | toaster.show({ 211 | title: "Approved Txn Hash", 212 | text: `Approved with hash: ${executedTxn.hash}`, 213 | }); 214 | loadApprovers(); 215 | } catch (e: any) { 216 | toaster.show({ 217 | title: "Error creating safe", 218 | text: `Message: ${e.message}`, 219 | }); 220 | } 221 | }, [proposal, safe, toaster, loadApprovers]); 222 | 223 | const signAndExecuteCallback = useCallback(async () => { 224 | if (!safe) return; 225 | try { 226 | const executedTxn = await signAndExecuteTx({ 227 | proposal: proposal!, 228 | safe, 229 | }); 230 | // call submit 231 | toaster.show({ 232 | title: "Executed Txn Hash", 233 | text: `Executed with hash: ${executedTxn.hash}`, 234 | }); 235 | 236 | loadApprovers(); 237 | } catch (e: any) { 238 | toaster.show({ 239 | title: "Error creating safe", 240 | text: `Message: ${e.message}`, 241 | }); 242 | } 243 | }, [proposal, safe, toaster, loadApprovers]); 244 | 245 | const hasApproved = address ? approvers.includes(address as Address) : false; 246 | 247 | // count others that are needed to sign, taking into account that self must have signed, and if self has signed 248 | // exclude from others needed to sign 249 | const canExecute = determineIfCanExecute({ 250 | hasApproved, 251 | totalApprovers: approvers.length, 252 | threshold: safeInformation?.threshold || 0, 253 | }); 254 | 255 | return ( 256 | <> 257 | 258 | Nonce: {proposal.nonce} 259 | {proposal.actions?.map((action, indx: number) => ( 260 | <> 261 | Proposal #{indx} 262 | To: {action.to as Address} 263 | Value: {formatEther(BigInt(action.value))} 264 | {action.data ? ( 265 | <> 266 | Data: {action.data} 267 | 268 | Data Actions:{" "} 269 |
270 |                     
274 |                   
275 |
276 | 277 | ) : ( 278 | No data 279 | )} 280 | 281 | ))} 282 |
283 | 284 | 285 | Approvers: ({approvers.length} out of {safeInformation?.threshold}{" "} 286 | signed) 287 | 288 | {approvers.map((approver) => ( 289 | 290 | {approver} {approver === address && "(you)"} 291 | 292 | ))} 293 | 294 | 295 | 296 | 299 | 302 | 303 | 304 | ); 305 | }; 306 | 307 | const EditProposal = ({ 308 | proposal, 309 | setProposal, 310 | }: { 311 | proposal: Proposal | undefined; 312 | setProposal: (proposal: Proposal) => void; 313 | }) => { 314 | const onSubmit = useCallback( 315 | (result: Proposal) => { 316 | const proposal = transformValuesToWei(result); 317 | if (proposal) { 318 | setProposal(proposal); 319 | } 320 | }, 321 | [setProposal], 322 | ); 323 | 324 | const defaultActions = proposal || DEFAULT_PROPOSAL; 325 | 326 | return ( 327 | 328 | 329 | 334 | {({ handleSubmit, values, isValid }) => ( 335 |
336 | 337 | 338 | New Proposal Details 339 | 340 | 341 | 342 | {GenericField({ 343 | label: "Nonce (optional)", 344 | fieldProps: { type: "number" }, 345 | })} 346 | 347 | 348 | 349 | {(actions) => ( 350 | <> 351 | {values.actions?.map((_, indx) => ( 352 | 357 | ))} 358 | 359 | 360 | 365 | 366 | 367 | )} 368 | 369 | 370 | 373 | 374 | 375 |
376 | )} 377 |
378 |
379 |
380 | ); 381 | }; 382 | 383 | export const NewSafeProposal = () => { 384 | const [isEditing, setIsEditing] = useState(true); 385 | 386 | const proposal = useLoadProposalFromQuery(); 387 | const { addAction, replace } = useUpdateProposalInQuery({ proposal }); 388 | 389 | useEffect(() => { 390 | if (proposal) { 391 | setIsEditing(false); 392 | } 393 | }, [proposal]); 394 | 395 | const handleEditClicked = useCallback( 396 | (evt: SyntheticEvent) => { 397 | setIsEditing(true); 398 | evt.preventDefault(); 399 | }, 400 | [setIsEditing], 401 | ); 402 | 403 | return ( 404 | 405 | 406 | {isEditing && ( 407 | 408 | )} 409 | {!isEditing && proposal && ( 410 | 414 | )} 415 | 416 | 417 | ); 418 | }; 419 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; /*! 2 | * Pico CSS v1.5.10 (https://picocss.com) 3 | * Copyright 2019-2023 - Licensed under MIT 4 | */ 5 | :root { 6 | --font-family: system-ui, -apple-system, "Segoe UI", "Roboto", "Ubuntu", 7 | "Cantarell", "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", 8 | "Segoe UI Symbol", "Noto Color Emoji"; 9 | --line-height: 1.5; 10 | --font-weight: 400; 11 | --font-size: 16px; 12 | --border-radius: 0.25rem; 13 | --border-width: 1px; 14 | --outline-width: 3px; 15 | --spacing: 1rem; 16 | --typography-spacing-vertical: 1.5rem; 17 | --block-spacing-vertical: calc(var(--spacing) * 2); 18 | --block-spacing-horizontal: var(--spacing); 19 | --grid-spacing-vertical: 0; 20 | --grid-spacing-horizontal: var(--spacing); 21 | --form-element-spacing-vertical: 0.75rem; 22 | --form-element-spacing-horizontal: 1rem; 23 | --nav-element-spacing-vertical: 1rem; 24 | --nav-element-spacing-horizontal: 0.5rem; 25 | --nav-link-spacing-vertical: 0.5rem; 26 | --nav-link-spacing-horizontal: 0.5rem; 27 | --form-label-font-weight: var(--font-weight); 28 | --transition: 0.2s ease-in-out; 29 | --modal-overlay-backdrop-filter: blur(0.25rem); 30 | } 31 | @media (min-width: 576px) { 32 | :root { 33 | --font-size: 17px; 34 | } 35 | } 36 | @media (min-width: 768px) { 37 | :root { 38 | --font-size: 18px; 39 | } 40 | } 41 | @media (min-width: 992px) { 42 | :root { 43 | --font-size: 19px; 44 | } 45 | } 46 | @media (min-width: 1200px) { 47 | :root { 48 | --font-size: 20px; 49 | } 50 | } 51 | @media (min-width: 576px) { 52 | body > footer, 53 | body > header, 54 | body > main, 55 | section { 56 | --block-spacing-vertical: calc(var(--spacing) * 2.5); 57 | } 58 | } 59 | @media (min-width: 768px) { 60 | body > footer, 61 | body > header, 62 | body > main, 63 | section { 64 | --block-spacing-vertical: calc(var(--spacing) * 3); 65 | } 66 | } 67 | @media (min-width: 992px) { 68 | body > footer, 69 | body > header, 70 | body > main, 71 | section { 72 | --block-spacing-vertical: calc(var(--spacing) * 3.5); 73 | } 74 | } 75 | @media (min-width: 1200px) { 76 | body > footer, 77 | body > header, 78 | body > main, 79 | section { 80 | --block-spacing-vertical: calc(var(--spacing) * 4); 81 | } 82 | } 83 | @media (min-width: 576px) { 84 | article { 85 | --block-spacing-horizontal: calc(var(--spacing) * 1.25); 86 | } 87 | } 88 | @media (min-width: 768px) { 89 | article { 90 | --block-spacing-horizontal: calc(var(--spacing) * 1.5); 91 | } 92 | } 93 | @media (min-width: 992px) { 94 | article { 95 | --block-spacing-horizontal: calc(var(--spacing) * 1.75); 96 | } 97 | } 98 | @media (min-width: 1200px) { 99 | article { 100 | --block-spacing-horizontal: calc(var(--spacing) * 2); 101 | } 102 | } 103 | dialog > article { 104 | --block-spacing-vertical: calc(var(--spacing) * 2); 105 | --block-spacing-horizontal: var(--spacing); 106 | } 107 | @media (min-width: 576px) { 108 | dialog > article { 109 | --block-spacing-vertical: calc(var(--spacing) * 2.5); 110 | --block-spacing-horizontal: calc(var(--spacing) * 1.25); 111 | } 112 | } 113 | @media (min-width: 768px) { 114 | dialog > article { 115 | --block-spacing-vertical: calc(var(--spacing) * 3); 116 | --block-spacing-horizontal: calc(var(--spacing) * 1.5); 117 | } 118 | } 119 | a { 120 | --text-decoration: none; 121 | } 122 | a.contrast, 123 | a.secondary { 124 | --text-decoration: underline; 125 | } 126 | small { 127 | --font-size: 0.875em; 128 | } 129 | h1, 130 | h2, 131 | h3, 132 | h4, 133 | h5, 134 | h6 { 135 | --font-weight: 700; 136 | } 137 | h1 { 138 | --font-size: 2rem; 139 | --typography-spacing-vertical: 3rem; 140 | } 141 | h2 { 142 | --font-size: 1.75rem; 143 | --typography-spacing-vertical: 2.625rem; 144 | } 145 | h3 { 146 | --font-size: 1.5rem; 147 | --typography-spacing-vertical: 2.25rem; 148 | } 149 | h4 { 150 | --font-size: 1.25rem; 151 | --typography-spacing-vertical: 1.874rem; 152 | } 153 | h5 { 154 | --font-size: 1.125rem; 155 | --typography-spacing-vertical: 1.6875rem; 156 | } 157 | [type="checkbox"], 158 | [type="radio"] { 159 | --border-width: 2px; 160 | } 161 | [type="checkbox"][role="switch"] { 162 | --border-width: 3px; 163 | } 164 | tfoot td, 165 | tfoot th, 166 | thead td, 167 | thead th { 168 | --border-width: 3px; 169 | } 170 | :not(thead, tfoot) > * > td { 171 | --font-size: 0.875em; 172 | } 173 | code, 174 | kbd, 175 | pre, 176 | samp { 177 | --font-family: "Menlo", "Consolas", "Roboto Mono", "Ubuntu Monospace", 178 | "Noto Mono", "Oxygen Mono", "Liberation Mono", monospace, 179 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 180 | } 181 | kbd { 182 | --font-weight: bolder; 183 | } 184 | :root:not([data-theme="dark"]), 185 | [data-theme="light"] { 186 | --background-color: #fff; 187 | --color: hsl(205, 20%, 32%); 188 | --h1-color: hsl(205, 30%, 15%); 189 | --h2-color: #24333e; 190 | --h3-color: hsl(205, 25%, 23%); 191 | --h4-color: #374956; 192 | --h5-color: hsl(205, 20%, 32%); 193 | --h6-color: #4d606d; 194 | --muted-color: hsl(205, 10%, 50%); 195 | --muted-border-color: hsl(205, 20%, 94%); 196 | --primary: hsl(195, 85%, 41%); 197 | --primary-hover: hsl(195, 90%, 32%); 198 | --primary-focus: rgba(16, 149, 193, 0.125); 199 | --primary-inverse: #fff; 200 | --secondary: hsl(205, 15%, 41%); 201 | --secondary-hover: hsl(205, 20%, 32%); 202 | --secondary-focus: rgba(89, 107, 120, 0.125); 203 | --secondary-inverse: #fff; 204 | --contrast: hsl(205, 30%, 15%); 205 | --contrast-hover: #000; 206 | --contrast-focus: rgba(89, 107, 120, 0.125); 207 | --contrast-inverse: #fff; 208 | --mark-background-color: #fff2ca; 209 | --mark-color: #543a26; 210 | --ins-color: #388e3c; 211 | --del-color: #c62828; 212 | --blockquote-border-color: var(--muted-border-color); 213 | --blockquote-footer-color: var(--muted-color); 214 | --button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); 215 | --button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); 216 | --form-element-background-color: transparent; 217 | --form-element-border-color: hsl(205, 14%, 68%); 218 | --form-element-color: var(--color); 219 | --form-element-placeholder-color: var(--muted-color); 220 | --form-element-active-background-color: transparent; 221 | --form-element-active-border-color: var(--primary); 222 | --form-element-focus-color: var(--primary-focus); 223 | --form-element-disabled-background-color: hsl(205, 18%, 86%); 224 | --form-element-disabled-border-color: hsl(205, 14%, 68%); 225 | --form-element-disabled-opacity: 0.5; 226 | --form-element-invalid-border-color: #c62828; 227 | --form-element-invalid-active-border-color: #d32f2f; 228 | --form-element-invalid-focus-color: rgba(211, 47, 47, 0.125); 229 | --form-element-valid-border-color: #388e3c; 230 | --form-element-valid-active-border-color: #43a047; 231 | --form-element-valid-focus-color: rgba(67, 160, 71, 0.125); 232 | --switch-background-color: hsl(205, 16%, 77%); 233 | --switch-color: var(--primary-inverse); 234 | --switch-checked-background-color: var(--primary); 235 | --range-border-color: hsl(205, 18%, 86%); 236 | --range-active-border-color: hsl(205, 16%, 77%); 237 | --range-thumb-border-color: var(--background-color); 238 | --range-thumb-color: var(--secondary); 239 | --range-thumb-hover-color: var(--secondary-hover); 240 | --range-thumb-active-color: var(--primary); 241 | --table-border-color: var(--muted-border-color); 242 | --table-row-stripped-background-color: #f6f8f9; 243 | --code-background-color: hsl(205, 20%, 94%); 244 | --code-color: var(--muted-color); 245 | --code-kbd-background-color: var(--contrast); 246 | --code-kbd-color: var(--contrast-inverse); 247 | --code-tag-color: hsl(330, 40%, 50%); 248 | --code-property-color: hsl(185, 40%, 40%); 249 | --code-value-color: hsl(40, 20%, 50%); 250 | --code-comment-color: hsl(205, 14%, 68%); 251 | --accordion-border-color: var(--muted-border-color); 252 | --accordion-close-summary-color: var(--color); 253 | --accordion-open-summary-color: var(--muted-color); 254 | --card-background-color: var(--background-color); 255 | --card-border-color: var(--muted-border-color); 256 | --card-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(27, 40, 50, 0.01698), 257 | 0.0335rem 0.067rem 0.402rem rgba(27, 40, 50, 0.024), 258 | 0.0625rem 0.125rem 0.75rem rgba(27, 40, 50, 0.03), 259 | 0.1125rem 0.225rem 1.35rem rgba(27, 40, 50, 0.036), 260 | 0.2085rem 0.417rem 2.502rem rgba(27, 40, 50, 0.04302), 261 | 0.5rem 1rem 6rem rgba(27, 40, 50, 0.06), 262 | 0 0 0 0.0625rem rgba(27, 40, 50, 0.015); 263 | --card-sectionning-background-color: #fbfbfc; 264 | --dropdown-background-color: #fbfbfc; 265 | --dropdown-border-color: #e1e6eb; 266 | --dropdown-box-shadow: var(--card-box-shadow); 267 | --dropdown-color: var(--color); 268 | --dropdown-hover-background-color: hsl(205, 20%, 94%); 269 | --modal-overlay-background-color: rgba(213, 220, 226, 0.7); 270 | --progress-background-color: hsl(205, 18%, 86%); 271 | --progress-color: var(--primary); 272 | --loading-spinner-opacity: 0.5; 273 | --tooltip-background-color: var(--contrast); 274 | --tooltip-color: var(--contrast-inverse); 275 | --icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); 276 | --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); 277 | --icon-chevron-button: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); 278 | --icon-chevron-button-inverse: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); 279 | --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E"); 280 | --icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E"); 281 | --icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(198, 40, 40)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); 282 | --icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E"); 283 | --icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); 284 | --icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E"); 285 | --icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(56, 142, 60)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); 286 | color-scheme: light; 287 | } 288 | @media only screen and (prefers-color-scheme: dark) { 289 | :root:not([data-theme]) { 290 | --background-color: #11191f; 291 | --color: hsl(205, 16%, 77%); 292 | --h1-color: hsl(205, 20%, 94%); 293 | --h2-color: #e1e6eb; 294 | --h3-color: hsl(205, 18%, 86%); 295 | --h4-color: #c8d1d8; 296 | --h5-color: hsl(205, 16%, 77%); 297 | --h6-color: #afbbc4; 298 | --muted-color: hsl(205, 10%, 50%); 299 | --muted-border-color: #1f2d38; 300 | --primary: hsl(195, 85%, 41%); 301 | --primary-hover: hsl(195, 80%, 50%); 302 | --primary-focus: rgba(16, 149, 193, 0.25); 303 | --primary-inverse: #fff; 304 | --secondary: hsl(205, 15%, 41%); 305 | --secondary-hover: hsl(205, 10%, 50%); 306 | --secondary-focus: rgba(115, 130, 140, 0.25); 307 | --secondary-inverse: #fff; 308 | --contrast: hsl(205, 20%, 94%); 309 | --contrast-hover: #fff; 310 | --contrast-focus: rgba(115, 130, 140, 0.25); 311 | --contrast-inverse: #000; 312 | --mark-background-color: #d1c284; 313 | --mark-color: #11191f; 314 | --ins-color: #388e3c; 315 | --del-color: #c62828; 316 | --blockquote-border-color: var(--muted-border-color); 317 | --blockquote-footer-color: var(--muted-color); 318 | --button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); 319 | --button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); 320 | --form-element-background-color: #11191f; 321 | --form-element-border-color: #374956; 322 | --form-element-color: var(--color); 323 | --form-element-placeholder-color: var(--muted-color); 324 | --form-element-active-background-color: var( 325 | --form-element-background-color 326 | ); 327 | --form-element-active-border-color: var(--primary); 328 | --form-element-focus-color: var(--primary-focus); 329 | --form-element-disabled-background-color: hsl(205, 25%, 23%); 330 | --form-element-disabled-border-color: hsl(205, 20%, 32%); 331 | --form-element-disabled-opacity: 0.5; 332 | --form-element-invalid-border-color: #b71c1c; 333 | --form-element-invalid-active-border-color: #c62828; 334 | --form-element-invalid-focus-color: rgba(198, 40, 40, 0.25); 335 | --form-element-valid-border-color: #2e7d32; 336 | --form-element-valid-active-border-color: #388e3c; 337 | --form-element-valid-focus-color: rgba(56, 142, 60, 0.25); 338 | --switch-background-color: #374956; 339 | --switch-color: var(--primary-inverse); 340 | --switch-checked-background-color: var(--primary); 341 | --range-border-color: #24333e; 342 | --range-active-border-color: hsl(205, 25%, 23%); 343 | --range-thumb-border-color: var(--background-color); 344 | --range-thumb-color: var(--secondary); 345 | --range-thumb-hover-color: var(--secondary-hover); 346 | --range-thumb-active-color: var(--primary); 347 | --table-border-color: var(--muted-border-color); 348 | --table-row-stripped-background-color: rgba(115, 130, 140, 0.05); 349 | --code-background-color: #18232c; 350 | --code-color: var(--muted-color); 351 | --code-kbd-background-color: var(--contrast); 352 | --code-kbd-color: var(--contrast-inverse); 353 | --code-tag-color: hsl(330, 30%, 50%); 354 | --code-property-color: hsl(185, 30%, 50%); 355 | --code-value-color: hsl(40, 10%, 50%); 356 | --code-comment-color: #4d606d; 357 | --accordion-border-color: var(--muted-border-color); 358 | --accordion-active-summary-color: var(--primary); 359 | --accordion-close-summary-color: var(--color); 360 | --accordion-open-summary-color: var(--muted-color); 361 | --card-background-color: #141e26; 362 | --card-border-color: var(--card-background-color); 363 | --card-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(0, 0, 0, 0.01698), 364 | 0.0335rem 0.067rem 0.402rem rgba(0, 0, 0, 0.024), 365 | 0.0625rem 0.125rem 0.75rem rgba(0, 0, 0, 0.03), 366 | 0.1125rem 0.225rem 1.35rem rgba(0, 0, 0, 0.036), 367 | 0.2085rem 0.417rem 2.502rem rgba(0, 0, 0, 0.04302), 368 | 0.5rem 1rem 6rem rgba(0, 0, 0, 0.06), 0 0 0 0.0625rem rgba(0, 0, 0, 0.015); 369 | --card-sectionning-background-color: #18232c; 370 | --dropdown-background-color: hsl(205, 30%, 15%); 371 | --dropdown-border-color: #24333e; 372 | --dropdown-box-shadow: var(--card-box-shadow); 373 | --dropdown-color: var(--color); 374 | --dropdown-hover-background-color: rgba(36, 51, 62, 0.75); 375 | --modal-overlay-background-color: rgba(36, 51, 62, 0.8); 376 | --progress-background-color: #24333e; 377 | --progress-color: var(--primary); 378 | --loading-spinner-opacity: 0.5; 379 | --tooltip-background-color: var(--contrast); 380 | --tooltip-color: var(--contrast-inverse); 381 | --icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); 382 | --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); 383 | --icon-chevron-button: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); 384 | --icon-chevron-button-inverse: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(0, 0, 0)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); 385 | --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E"); 386 | --icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E"); 387 | --icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(183, 28, 28)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); 388 | --icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E"); 389 | --icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); 390 | --icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E"); 391 | --icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(46, 125, 50)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); 392 | color-scheme: dark; 393 | } 394 | } 395 | [data-theme="dark"] { 396 | --background-color: #11191f; 397 | --color: hsl(205, 16%, 77%); 398 | --h1-color: hsl(205, 20%, 94%); 399 | --h2-color: #e1e6eb; 400 | --h3-color: hsl(205, 18%, 86%); 401 | --h4-color: #c8d1d8; 402 | --h5-color: hsl(205, 16%, 77%); 403 | --h6-color: #afbbc4; 404 | --muted-color: hsl(205, 10%, 50%); 405 | --muted-border-color: #1f2d38; 406 | --primary: hsl(195, 85%, 41%); 407 | --primary-hover: hsl(195, 80%, 50%); 408 | --primary-focus: rgba(16, 149, 193, 0.25); 409 | --primary-inverse: #fff; 410 | --secondary: hsl(205, 15%, 41%); 411 | --secondary-hover: hsl(205, 10%, 50%); 412 | --secondary-focus: rgba(115, 130, 140, 0.25); 413 | --secondary-inverse: #fff; 414 | --contrast: hsl(205, 20%, 94%); 415 | --contrast-hover: #fff; 416 | --contrast-focus: rgba(115, 130, 140, 0.25); 417 | --contrast-inverse: #000; 418 | --mark-background-color: #d1c284; 419 | --mark-color: #11191f; 420 | --ins-color: #388e3c; 421 | --del-color: #c62828; 422 | --blockquote-border-color: var(--muted-border-color); 423 | --blockquote-footer-color: var(--muted-color); 424 | --button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); 425 | --button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); 426 | --form-element-background-color: #11191f; 427 | --form-element-border-color: #374956; 428 | --form-element-color: var(--color); 429 | --form-element-placeholder-color: var(--muted-color); 430 | --form-element-active-background-color: var(--form-element-background-color); 431 | --form-element-active-border-color: var(--primary); 432 | --form-element-focus-color: var(--primary-focus); 433 | --form-element-disabled-background-color: hsl(205, 25%, 23%); 434 | --form-element-disabled-border-color: hsl(205, 20%, 32%); 435 | --form-element-disabled-opacity: 0.5; 436 | --form-element-invalid-border-color: #b71c1c; 437 | --form-element-invalid-active-border-color: #c62828; 438 | --form-element-invalid-focus-color: rgba(198, 40, 40, 0.25); 439 | --form-element-valid-border-color: #2e7d32; 440 | --form-element-valid-active-border-color: #388e3c; 441 | --form-element-valid-focus-color: rgba(56, 142, 60, 0.25); 442 | --switch-background-color: #374956; 443 | --switch-color: var(--primary-inverse); 444 | --switch-checked-background-color: var(--primary); 445 | --range-border-color: #24333e; 446 | --range-active-border-color: hsl(205, 25%, 23%); 447 | --range-thumb-border-color: var(--background-color); 448 | --range-thumb-color: var(--secondary); 449 | --range-thumb-hover-color: var(--secondary-hover); 450 | --range-thumb-active-color: var(--primary); 451 | --table-border-color: var(--muted-border-color); 452 | --table-row-stripped-background-color: rgba(115, 130, 140, 0.05); 453 | --code-background-color: #18232c; 454 | --code-color: var(--muted-color); 455 | --code-kbd-background-color: var(--contrast); 456 | --code-kbd-color: var(--contrast-inverse); 457 | --code-tag-color: hsl(330, 30%, 50%); 458 | --code-property-color: hsl(185, 30%, 50%); 459 | --code-value-color: hsl(40, 10%, 50%); 460 | --code-comment-color: #4d606d; 461 | --accordion-border-color: var(--muted-border-color); 462 | --accordion-active-summary-color: var(--primary); 463 | --accordion-close-summary-color: var(--color); 464 | --accordion-open-summary-color: var(--muted-color); 465 | --card-background-color: #141e26; 466 | --card-border-color: var(--card-background-color); 467 | --card-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(0, 0, 0, 0.01698), 468 | 0.0335rem 0.067rem 0.402rem rgba(0, 0, 0, 0.024), 469 | 0.0625rem 0.125rem 0.75rem rgba(0, 0, 0, 0.03), 470 | 0.1125rem 0.225rem 1.35rem rgba(0, 0, 0, 0.036), 471 | 0.2085rem 0.417rem 2.502rem rgba(0, 0, 0, 0.04302), 472 | 0.5rem 1rem 6rem rgba(0, 0, 0, 0.06), 0 0 0 0.0625rem rgba(0, 0, 0, 0.015); 473 | --card-sectionning-background-color: #18232c; 474 | --dropdown-background-color: hsl(205, 30%, 15%); 475 | --dropdown-border-color: #24333e; 476 | --dropdown-box-shadow: var(--card-box-shadow); 477 | --dropdown-color: var(--color); 478 | --dropdown-hover-background-color: rgba(36, 51, 62, 0.75); 479 | --modal-overlay-background-color: rgba(36, 51, 62, 0.8); 480 | --progress-background-color: #24333e; 481 | --progress-color: var(--primary); 482 | --loading-spinner-opacity: 0.5; 483 | --tooltip-background-color: var(--contrast); 484 | --tooltip-color: var(--contrast-inverse); 485 | --icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); 486 | --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); 487 | --icon-chevron-button: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); 488 | --icon-chevron-button-inverse: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(0, 0, 0)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); 489 | --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E"); 490 | --icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E"); 491 | --icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(183, 28, 28)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); 492 | --icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E"); 493 | --icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); 494 | --icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E"); 495 | --icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(46, 125, 50)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); 496 | color-scheme: dark; 497 | } 498 | [type="checkbox"], 499 | [type="radio"], 500 | [type="range"], 501 | progress { 502 | accent-color: var(--primary); 503 | } 504 | *, 505 | ::after, 506 | ::before { 507 | box-sizing: border-box; 508 | background-repeat: no-repeat; 509 | } 510 | ::after, 511 | ::before { 512 | text-decoration: inherit; 513 | vertical-align: inherit; 514 | } 515 | :where(:root) { 516 | -webkit-tap-highlight-color: transparent; 517 | -webkit-text-size-adjust: 100%; 518 | -moz-text-size-adjust: 100%; 519 | text-size-adjust: 100%; 520 | background-color: var(--background-color); 521 | color: var(--color); 522 | font-weight: var(--font-weight); 523 | font-size: var(--font-size); 524 | line-height: var(--line-height); 525 | font-family: var(--font-family); 526 | text-rendering: optimizeLegibility; 527 | overflow-wrap: break-word; 528 | cursor: default; 529 | -moz-tab-size: 4; 530 | -o-tab-size: 4; 531 | tab-size: 4; 532 | } 533 | main { 534 | display: block; 535 | } 536 | body { 537 | width: 100%; 538 | margin: 0; 539 | } 540 | body > footer, 541 | body > header, 542 | body > main { 543 | width: 100%; 544 | margin-right: auto; 545 | margin-left: auto; 546 | padding: var(--block-spacing-vertical) 0; 547 | } 548 | .container, 549 | .container-fluid { 550 | width: 100%; 551 | margin-right: auto; 552 | margin-left: auto; 553 | padding-right: var(--spacing); 554 | padding-left: var(--spacing); 555 | } 556 | @media (min-width: 576px) { 557 | .container { 558 | max-width: 510px; 559 | padding-right: 0; 560 | padding-left: 0; 561 | } 562 | } 563 | @media (min-width: 768px) { 564 | .container { 565 | max-width: 700px; 566 | } 567 | } 568 | @media (min-width: 992px) { 569 | .container { 570 | max-width: 920px; 571 | } 572 | } 573 | @media (min-width: 1200px) { 574 | .container { 575 | max-width: 1130px; 576 | } 577 | } 578 | section { 579 | margin-bottom: var(--block-spacing-vertical); 580 | } 581 | .grid { 582 | grid-column-gap: var(--grid-spacing-horizontal); 583 | grid-row-gap: var(--grid-spacing-vertical); 584 | display: grid; 585 | grid-template-columns: 1fr; 586 | margin: 0; 587 | } 588 | @media (min-width: 992px) { 589 | .grid { 590 | grid-template-columns: repeat(auto-fit, minmax(0%, 1fr)); 591 | } 592 | } 593 | .grid > * { 594 | min-width: 0; 595 | } 596 | figure { 597 | display: block; 598 | margin: 0; 599 | padding: 0; 600 | overflow-x: auto; 601 | } 602 | figure figcaption { 603 | padding: calc(var(--spacing) * 0.5) 0; 604 | color: var(--muted-color); 605 | } 606 | b, 607 | strong { 608 | font-weight: bolder; 609 | } 610 | sub, 611 | sup { 612 | position: relative; 613 | font-size: 0.75em; 614 | line-height: 0; 615 | vertical-align: baseline; 616 | } 617 | sub { 618 | bottom: -0.25em; 619 | } 620 | sup { 621 | top: -0.5em; 622 | } 623 | address, 624 | blockquote, 625 | dl, 626 | figure, 627 | form, 628 | ol, 629 | p, 630 | pre, 631 | table, 632 | ul { 633 | margin-top: 0; 634 | margin-bottom: var(--typography-spacing-vertical); 635 | color: var(--color); 636 | font-style: normal; 637 | font-weight: var(--font-weight); 638 | font-size: var(--font-size); 639 | } 640 | [role="link"], 641 | a { 642 | --color: var(--primary); 643 | --background-color: transparent; 644 | outline: 0; 645 | background-color: var(--background-color); 646 | color: var(--color); 647 | -webkit-text-decoration: var(--text-decoration); 648 | text-decoration: var(--text-decoration); 649 | transition: 650 | background-color var(--transition), 651 | color var(--transition), 652 | box-shadow var(--transition), 653 | -webkit-text-decoration var(--transition); 654 | transition: 655 | background-color var(--transition), 656 | color var(--transition), 657 | text-decoration var(--transition), 658 | box-shadow var(--transition); 659 | transition: 660 | background-color var(--transition), 661 | color var(--transition), 662 | text-decoration var(--transition), 663 | box-shadow var(--transition), 664 | -webkit-text-decoration var(--transition); 665 | } 666 | [role="link"]:is([aria-current], :hover, :active, :focus), 667 | a:is([aria-current], :hover, :active, :focus) { 668 | --color: var(--primary-hover); 669 | --text-decoration: underline; 670 | } 671 | [role="link"]:focus, 672 | a:focus { 673 | --background-color: var(--primary-focus); 674 | } 675 | [role="link"].secondary, 676 | a.secondary { 677 | --color: var(--secondary); 678 | } 679 | [role="link"].secondary:is([aria-current], :hover, :active, :focus), 680 | a.secondary:is([aria-current], :hover, :active, :focus) { 681 | --color: var(--secondary-hover); 682 | } 683 | [role="link"].secondary:focus, 684 | a.secondary:focus { 685 | --background-color: var(--secondary-focus); 686 | } 687 | [role="link"].contrast, 688 | a.contrast { 689 | --color: var(--contrast); 690 | } 691 | [role="link"].contrast:is([aria-current], :hover, :active, :focus), 692 | a.contrast:is([aria-current], :hover, :active, :focus) { 693 | --color: var(--contrast-hover); 694 | } 695 | [role="link"].contrast:focus, 696 | a.contrast:focus { 697 | --background-color: var(--contrast-focus); 698 | } 699 | h1, 700 | h2, 701 | h3, 702 | h4, 703 | h5, 704 | h6 { 705 | margin-top: 0; 706 | margin-bottom: var(--typography-spacing-vertical); 707 | color: var(--color); 708 | font-weight: var(--font-weight); 709 | font-size: var(--font-size); 710 | font-family: var(--font-family); 711 | } 712 | h1 { 713 | --color: var(--h1-color); 714 | } 715 | h2 { 716 | --color: var(--h2-color); 717 | } 718 | h3 { 719 | --color: var(--h3-color); 720 | } 721 | h4 { 722 | --color: var(--h4-color); 723 | } 724 | h5 { 725 | --color: var(--h5-color); 726 | } 727 | h6 { 728 | --color: var(--h6-color); 729 | } 730 | :where(address, blockquote, dl, figure, form, ol, p, pre, table, ul) 731 | ~ :is(h1, h2, h3, h4, h5, h6) { 732 | margin-top: var(--typography-spacing-vertical); 733 | } 734 | .headings, 735 | hgroup { 736 | margin-bottom: var(--typography-spacing-vertical); 737 | } 738 | .headings > *, 739 | hgroup > * { 740 | margin-bottom: 0; 741 | } 742 | .headings > :last-child, 743 | hgroup > :last-child { 744 | --color: var(--muted-color); 745 | --font-weight: unset; 746 | font-size: 1rem; 747 | font-family: unset; 748 | } 749 | p { 750 | margin-bottom: var(--typography-spacing-vertical); 751 | } 752 | small { 753 | font-size: var(--font-size); 754 | } 755 | :where(dl, ol, ul) { 756 | padding-right: 0; 757 | padding-left: var(--spacing); 758 | -webkit-padding-start: var(--spacing); 759 | padding-inline-start: var(--spacing); 760 | -webkit-padding-end: 0; 761 | padding-inline-end: 0; 762 | } 763 | :where(dl, ol, ul) li { 764 | margin-bottom: calc(var(--typography-spacing-vertical) * 0.25); 765 | } 766 | :where(dl, ol, ul) :is(dl, ol, ul) { 767 | margin: 0; 768 | margin-top: calc(var(--typography-spacing-vertical) * 0.25); 769 | } 770 | ul li { 771 | list-style: square; 772 | } 773 | mark { 774 | padding: 0.125rem 0.25rem; 775 | background-color: var(--mark-background-color); 776 | color: var(--mark-color); 777 | vertical-align: baseline; 778 | } 779 | blockquote { 780 | display: block; 781 | margin: var(--typography-spacing-vertical) 0; 782 | padding: var(--spacing); 783 | border-right: none; 784 | border-left: 0.25rem solid var(--blockquote-border-color); 785 | -webkit-border-start: 0.25rem solid var(--blockquote-border-color); 786 | border-inline-start: 0.25rem solid var(--blockquote-border-color); 787 | -webkit-border-end: none; 788 | border-inline-end: none; 789 | } 790 | blockquote footer { 791 | margin-top: calc(var(--typography-spacing-vertical) * 0.5); 792 | color: var(--blockquote-footer-color); 793 | } 794 | abbr[title] { 795 | border-bottom: 1px dotted; 796 | text-decoration: none; 797 | cursor: help; 798 | } 799 | ins { 800 | color: var(--ins-color); 801 | text-decoration: none; 802 | } 803 | del { 804 | color: var(--del-color); 805 | } 806 | ::-moz-selection { 807 | background-color: var(--primary-focus); 808 | } 809 | ::selection { 810 | background-color: var(--primary-focus); 811 | } 812 | :where(audio, canvas, iframe, img, svg, video) { 813 | vertical-align: middle; 814 | } 815 | audio, 816 | video { 817 | display: inline-block; 818 | } 819 | audio:not([controls]) { 820 | display: none; 821 | height: 0; 822 | } 823 | :where(iframe) { 824 | border-style: none; 825 | } 826 | img { 827 | max-width: 100%; 828 | height: auto; 829 | border-style: none; 830 | } 831 | :where(svg:not([fill])) { 832 | fill: currentColor; 833 | } 834 | svg:not(:root) { 835 | overflow: hidden; 836 | } 837 | button { 838 | margin: 0; 839 | overflow: visible; 840 | font-family: inherit; 841 | text-transform: none; 842 | } 843 | [type="button"], 844 | [type="reset"], 845 | [type="submit"], 846 | button { 847 | -webkit-appearance: button; 848 | } 849 | button { 850 | display: block; 851 | width: 100%; 852 | margin-bottom: var(--spacing); 853 | } 854 | [role="button"] { 855 | display: inline-block; 856 | text-decoration: none; 857 | } 858 | [role="button"], 859 | button, 860 | input[type="button"], 861 | input[type="reset"], 862 | input[type="submit"] { 863 | --background-color: var(--primary); 864 | --border-color: var(--primary); 865 | --color: var(--primary-inverse); 866 | --box-shadow: var(--button-box-shadow, 0 0 0 rgba(0, 0, 0, 0)); 867 | padding: var(--form-element-spacing-vertical) 868 | var(--form-element-spacing-horizontal); 869 | border: var(--border-width) solid var(--border-color); 870 | border-radius: var(--border-radius); 871 | outline: 0; 872 | background-color: var(--background-color); 873 | box-shadow: var(--box-shadow); 874 | color: var(--color); 875 | font-weight: var(--font-weight); 876 | font-size: 1rem; 877 | line-height: var(--line-height); 878 | text-align: center; 879 | cursor: pointer; 880 | transition: 881 | background-color var(--transition), 882 | border-color var(--transition), 883 | color var(--transition), 884 | box-shadow var(--transition); 885 | } 886 | [role="button"]:is([aria-current], :hover, :active, :focus), 887 | button:is([aria-current], :hover, :active, :focus), 888 | input[type="button"]:is([aria-current], :hover, :active, :focus), 889 | input[type="reset"]:is([aria-current], :hover, :active, :focus), 890 | input[type="submit"]:is([aria-current], :hover, :active, :focus) { 891 | --background-color: var(--primary-hover); 892 | --border-color: var(--primary-hover); 893 | --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)); 894 | --color: var(--primary-inverse); 895 | } 896 | [role="button"]:focus, 897 | button:focus, 898 | input[type="button"]:focus, 899 | input[type="reset"]:focus, 900 | input[type="submit"]:focus { 901 | --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), 902 | 0 0 0 var(--outline-width) var(--primary-focus); 903 | } 904 | :is( 905 | button, 906 | input[type="submit"], 907 | input[type="button"], 908 | [role="button"] 909 | ).secondary, 910 | input[type="reset"] { 911 | --background-color: var(--secondary); 912 | --border-color: var(--secondary); 913 | --color: var(--secondary-inverse); 914 | cursor: pointer; 915 | } 916 | :is( 917 | button, 918 | input[type="submit"], 919 | input[type="button"], 920 | [role="button"] 921 | ).secondary:is([aria-current], :hover, :active, :focus), 922 | input[type="reset"]:is([aria-current], :hover, :active, :focus) { 923 | --background-color: var(--secondary-hover); 924 | --border-color: var(--secondary-hover); 925 | --color: var(--secondary-inverse); 926 | } 927 | :is( 928 | button, 929 | input[type="submit"], 930 | input[type="button"], 931 | [role="button"] 932 | ).secondary:focus, 933 | input[type="reset"]:focus { 934 | --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), 935 | 0 0 0 var(--outline-width) var(--secondary-focus); 936 | } 937 | :is( 938 | button, 939 | input[type="submit"], 940 | input[type="button"], 941 | [role="button"] 942 | ).contrast { 943 | --background-color: var(--contrast); 944 | --border-color: var(--contrast); 945 | --color: var(--contrast-inverse); 946 | } 947 | :is( 948 | button, 949 | input[type="submit"], 950 | input[type="button"], 951 | [role="button"] 952 | ).contrast:is([aria-current], :hover, :active, :focus) { 953 | --background-color: var(--contrast-hover); 954 | --border-color: var(--contrast-hover); 955 | --color: var(--contrast-inverse); 956 | } 957 | :is( 958 | button, 959 | input[type="submit"], 960 | input[type="button"], 961 | [role="button"] 962 | ).contrast:focus { 963 | --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), 964 | 0 0 0 var(--outline-width) var(--contrast-focus); 965 | } 966 | :is( 967 | button, 968 | input[type="submit"], 969 | input[type="button"], 970 | [role="button"] 971 | ).outline, 972 | input[type="reset"].outline { 973 | --background-color: transparent; 974 | --color: var(--primary); 975 | } 976 | :is( 977 | button, 978 | input[type="submit"], 979 | input[type="button"], 980 | [role="button"] 981 | ).outline:is([aria-current], :hover, :active, :focus), 982 | input[type="reset"].outline:is([aria-current], :hover, :active, :focus) { 983 | --background-color: transparent; 984 | --color: var(--primary-hover); 985 | } 986 | :is( 987 | button, 988 | input[type="submit"], 989 | input[type="button"], 990 | [role="button"] 991 | ).outline.secondary, 992 | input[type="reset"].outline { 993 | --color: var(--secondary); 994 | } 995 | :is( 996 | button, 997 | input[type="submit"], 998 | input[type="button"], 999 | [role="button"] 1000 | ).outline.secondary:is([aria-current], :hover, :active, :focus), 1001 | input[type="reset"].outline:is([aria-current], :hover, :active, :focus) { 1002 | --color: var(--secondary-hover); 1003 | } 1004 | :is( 1005 | button, 1006 | input[type="submit"], 1007 | input[type="button"], 1008 | [role="button"] 1009 | ).outline.contrast { 1010 | --color: var(--contrast); 1011 | } 1012 | :is( 1013 | button, 1014 | input[type="submit"], 1015 | input[type="button"], 1016 | [role="button"] 1017 | ).outline.contrast:is([aria-current], :hover, :active, :focus) { 1018 | --color: var(--contrast-hover); 1019 | } 1020 | :where( 1021 | button, 1022 | [type="submit"], 1023 | [type="button"], 1024 | [type="reset"], 1025 | [role="button"] 1026 | )[disabled], 1027 | :where(fieldset[disabled]) 1028 | :is( 1029 | button, 1030 | [type="submit"], 1031 | [type="button"], 1032 | [type="reset"], 1033 | [role="button"] 1034 | ), 1035 | a[role="button"]:not([href]) { 1036 | opacity: 0.5; 1037 | pointer-events: none; 1038 | } 1039 | input, 1040 | optgroup, 1041 | select, 1042 | textarea { 1043 | margin: 0; 1044 | font-size: 1rem; 1045 | line-height: var(--line-height); 1046 | font-family: inherit; 1047 | letter-spacing: inherit; 1048 | } 1049 | input { 1050 | overflow: visible; 1051 | } 1052 | select { 1053 | text-transform: none; 1054 | } 1055 | legend { 1056 | max-width: 100%; 1057 | padding: 0; 1058 | color: inherit; 1059 | white-space: normal; 1060 | } 1061 | textarea { 1062 | overflow: auto; 1063 | } 1064 | [type="checkbox"], 1065 | [type="radio"] { 1066 | padding: 0; 1067 | } 1068 | ::-webkit-inner-spin-button, 1069 | ::-webkit-outer-spin-button { 1070 | height: auto; 1071 | } 1072 | [type="search"] { 1073 | -webkit-appearance: textfield; 1074 | outline-offset: -2px; 1075 | } 1076 | [type="search"]::-webkit-search-decoration { 1077 | -webkit-appearance: none; 1078 | } 1079 | ::-webkit-file-upload-button { 1080 | -webkit-appearance: button; 1081 | font: inherit; 1082 | } 1083 | ::-moz-focus-inner { 1084 | padding: 0; 1085 | border-style: none; 1086 | } 1087 | :-moz-focusring { 1088 | outline: 0; 1089 | } 1090 | :-moz-ui-invalid { 1091 | box-shadow: none; 1092 | } 1093 | ::-ms-expand { 1094 | display: none; 1095 | } 1096 | [type="file"], 1097 | [type="range"] { 1098 | padding: 0; 1099 | border-width: 0; 1100 | } 1101 | input:not([type="checkbox"], [type="radio"], [type="range"]) { 1102 | height: calc( 1103 | 1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + 1104 | var(--border-width) * 2 1105 | ); 1106 | } 1107 | fieldset { 1108 | margin: 0; 1109 | margin-bottom: var(--spacing); 1110 | padding: 0; 1111 | border: 0; 1112 | } 1113 | fieldset legend, 1114 | label { 1115 | display: block; 1116 | margin-bottom: calc(var(--spacing) * 0.25); 1117 | font-weight: var(--form-label-font-weight, var(--font-weight)); 1118 | } 1119 | input:not([type="checkbox"], [type="radio"]), 1120 | select, 1121 | textarea { 1122 | width: 100%; 1123 | } 1124 | input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"]), 1125 | select, 1126 | textarea { 1127 | -webkit-appearance: none; 1128 | -moz-appearance: none; 1129 | appearance: none; 1130 | padding: var(--form-element-spacing-vertical) 1131 | var(--form-element-spacing-horizontal); 1132 | } 1133 | input, 1134 | select, 1135 | textarea { 1136 | --background-color: var(--form-element-background-color); 1137 | --border-color: var(--form-element-border-color); 1138 | --color: var(--form-element-color); 1139 | --box-shadow: none; 1140 | border: var(--border-width) solid var(--border-color); 1141 | border-radius: var(--border-radius); 1142 | outline: 0; 1143 | background-color: var(--background-color); 1144 | box-shadow: var(--box-shadow); 1145 | color: var(--color); 1146 | font-weight: var(--font-weight); 1147 | transition: 1148 | background-color var(--transition), 1149 | border-color var(--transition), 1150 | color var(--transition), 1151 | box-shadow var(--transition); 1152 | } 1153 | :where(select, textarea):is(:active, :focus), 1154 | input:not( 1155 | [type="submit"], 1156 | [type="button"], 1157 | [type="reset"], 1158 | [type="checkbox"], 1159 | [type="radio"], 1160 | [readonly] 1161 | ):is(:active, :focus) { 1162 | --background-color: var(--form-element-active-background-color); 1163 | } 1164 | :where(select, textarea):is(:active, :focus), 1165 | input:not( 1166 | [type="submit"], 1167 | [type="button"], 1168 | [type="reset"], 1169 | [role="switch"], 1170 | [readonly] 1171 | ):is(:active, :focus) { 1172 | --border-color: var(--form-element-active-border-color); 1173 | } 1174 | input:not( 1175 | [type="submit"], 1176 | [type="button"], 1177 | [type="reset"], 1178 | [type="range"], 1179 | [type="file"], 1180 | [readonly] 1181 | ):focus, 1182 | select:focus, 1183 | textarea:focus { 1184 | --box-shadow: 0 0 0 var(--outline-width) var(--form-element-focus-color); 1185 | } 1186 | :where(fieldset[disabled]) 1187 | :is( 1188 | input:not([type="submit"], [type="button"], [type="reset"]), 1189 | select, 1190 | textarea 1191 | ), 1192 | input:not([type="submit"], [type="button"], [type="reset"])[disabled], 1193 | select[disabled], 1194 | textarea[disabled] { 1195 | --background-color: var(--form-element-disabled-background-color); 1196 | --border-color: var(--form-element-disabled-border-color); 1197 | opacity: var(--form-element-disabled-opacity); 1198 | pointer-events: none; 1199 | } 1200 | :where(input, select, textarea):not( 1201 | [type="checkbox"], 1202 | [type="radio"], 1203 | [type="date"], 1204 | [type="datetime-local"], 1205 | [type="month"], 1206 | [type="time"], 1207 | [type="week"] 1208 | )[aria-invalid] { 1209 | padding-right: calc( 1210 | var(--form-element-spacing-horizontal) + 1.5rem 1211 | ) !important; 1212 | padding-left: var(--form-element-spacing-horizontal); 1213 | -webkit-padding-start: var(--form-element-spacing-horizontal) !important; 1214 | padding-inline-start: var(--form-element-spacing-horizontal) !important; 1215 | -webkit-padding-end: calc( 1216 | var(--form-element-spacing-horizontal) + 1.5rem 1217 | ) !important; 1218 | padding-inline-end: calc( 1219 | var(--form-element-spacing-horizontal) + 1.5rem 1220 | ) !important; 1221 | background-position: center right 0.75rem; 1222 | background-size: 1rem auto; 1223 | background-repeat: no-repeat; 1224 | } 1225 | :where(input, select, textarea):not( 1226 | [type="checkbox"], 1227 | [type="radio"], 1228 | [type="date"], 1229 | [type="datetime-local"], 1230 | [type="month"], 1231 | [type="time"], 1232 | [type="week"] 1233 | )[aria-invalid="false"] { 1234 | background-image: var(--icon-valid); 1235 | } 1236 | :where(input, select, textarea):not( 1237 | [type="checkbox"], 1238 | [type="radio"], 1239 | [type="date"], 1240 | [type="datetime-local"], 1241 | [type="month"], 1242 | [type="time"], 1243 | [type="week"] 1244 | )[aria-invalid="true"] { 1245 | background-image: var(--icon-invalid); 1246 | } 1247 | :where(input, select, textarea)[aria-invalid="false"] { 1248 | --border-color: var(--form-element-valid-border-color); 1249 | } 1250 | :where(input, select, textarea)[aria-invalid="false"]:is(:active, :focus) { 1251 | --border-color: var(--form-element-valid-active-border-color) !important; 1252 | --box-shadow: 0 0 0 var(--outline-width) var(--form-element-valid-focus-color) !important; 1253 | } 1254 | :where(input, select, textarea)[aria-invalid="true"] { 1255 | --border-color: var(--form-element-invalid-border-color); 1256 | } 1257 | :where(input, select, textarea)[aria-invalid="true"]:is(:active, :focus) { 1258 | --border-color: var(--form-element-invalid-active-border-color) !important; 1259 | --box-shadow: 0 0 0 var(--outline-width) 1260 | var(--form-element-invalid-focus-color) !important; 1261 | } 1262 | [dir="rtl"] 1263 | :where(input, select, textarea):not([type="checkbox"], [type="radio"]):is( 1264 | [aria-invalid], 1265 | [aria-invalid="true"], 1266 | [aria-invalid="false"] 1267 | ) { 1268 | background-position: center left 0.75rem; 1269 | } 1270 | input::-webkit-input-placeholder, 1271 | input::placeholder, 1272 | select:invalid, 1273 | textarea::-webkit-input-placeholder, 1274 | textarea::placeholder { 1275 | color: var(--form-element-placeholder-color); 1276 | opacity: 1; 1277 | } 1278 | input:not([type="checkbox"], [type="radio"]), 1279 | select, 1280 | textarea { 1281 | margin-bottom: var(--spacing); 1282 | } 1283 | select::-ms-expand { 1284 | border: 0; 1285 | background-color: transparent; 1286 | } 1287 | select:not([multiple], [size]) { 1288 | padding-right: calc(var(--form-element-spacing-horizontal) + 1.5rem); 1289 | padding-left: var(--form-element-spacing-horizontal); 1290 | -webkit-padding-start: var(--form-element-spacing-horizontal); 1291 | padding-inline-start: var(--form-element-spacing-horizontal); 1292 | -webkit-padding-end: calc(var(--form-element-spacing-horizontal) + 1.5rem); 1293 | padding-inline-end: calc(var(--form-element-spacing-horizontal) + 1.5rem); 1294 | background-image: var(--icon-chevron); 1295 | background-position: center right 0.75rem; 1296 | background-size: 1rem auto; 1297 | background-repeat: no-repeat; 1298 | } 1299 | [dir="rtl"] select:not([multiple], [size]) { 1300 | background-position: center left 0.75rem; 1301 | } 1302 | :where(input, select, textarea, .grid) + small { 1303 | display: block; 1304 | width: 100%; 1305 | margin-top: calc(var(--spacing) * -0.75); 1306 | margin-bottom: var(--spacing); 1307 | color: var(--muted-color); 1308 | } 1309 | label > :where(input, select, textarea) { 1310 | margin-top: calc(var(--spacing) * 0.25); 1311 | } 1312 | [type="checkbox"], 1313 | [type="radio"] { 1314 | -webkit-appearance: none; 1315 | -moz-appearance: none; 1316 | appearance: none; 1317 | width: 1.25em; 1318 | height: 1.25em; 1319 | margin-top: -0.125em; 1320 | margin-right: 0.375em; 1321 | margin-left: 0; 1322 | -webkit-margin-start: 0; 1323 | margin-inline-start: 0; 1324 | -webkit-margin-end: 0.375em; 1325 | margin-inline-end: 0.375em; 1326 | border-width: var(--border-width); 1327 | font-size: inherit; 1328 | vertical-align: middle; 1329 | cursor: pointer; 1330 | } 1331 | [type="checkbox"]::-ms-check, 1332 | [type="radio"]::-ms-check { 1333 | display: none; 1334 | } 1335 | [type="checkbox"]:checked, 1336 | [type="checkbox"]:checked:active, 1337 | [type="checkbox"]:checked:focus, 1338 | [type="radio"]:checked, 1339 | [type="radio"]:checked:active, 1340 | [type="radio"]:checked:focus { 1341 | --background-color: var(--primary); 1342 | --border-color: var(--primary); 1343 | background-image: var(--icon-checkbox); 1344 | background-position: center; 1345 | background-size: 0.75em auto; 1346 | background-repeat: no-repeat; 1347 | } 1348 | [type="checkbox"] ~ label, 1349 | [type="radio"] ~ label { 1350 | display: inline-block; 1351 | margin-right: 0.375em; 1352 | margin-bottom: 0; 1353 | cursor: pointer; 1354 | } 1355 | [type="checkbox"]:indeterminate { 1356 | --background-color: var(--primary); 1357 | --border-color: var(--primary); 1358 | background-image: var(--icon-minus); 1359 | background-position: center; 1360 | background-size: 0.75em auto; 1361 | background-repeat: no-repeat; 1362 | } 1363 | [type="radio"] { 1364 | border-radius: 50%; 1365 | } 1366 | [type="radio"]:checked, 1367 | [type="radio"]:checked:active, 1368 | [type="radio"]:checked:focus { 1369 | --background-color: var(--primary-inverse); 1370 | border-width: 0.35em; 1371 | background-image: none; 1372 | } 1373 | [type="checkbox"][role="switch"] { 1374 | --background-color: var(--switch-background-color); 1375 | --border-color: var(--switch-background-color); 1376 | --color: var(--switch-color); 1377 | width: 2.25em; 1378 | height: 1.25em; 1379 | border: var(--border-width) solid var(--border-color); 1380 | border-radius: 1.25em; 1381 | background-color: var(--background-color); 1382 | line-height: 1.25em; 1383 | } 1384 | [type="checkbox"][role="switch"]:focus { 1385 | --background-color: var(--switch-background-color); 1386 | --border-color: var(--switch-background-color); 1387 | } 1388 | [type="checkbox"][role="switch"]:checked { 1389 | --background-color: var(--switch-checked-background-color); 1390 | --border-color: var(--switch-checked-background-color); 1391 | } 1392 | [type="checkbox"][role="switch"]:before { 1393 | display: block; 1394 | width: calc(1.25em - (var(--border-width) * 2)); 1395 | height: 100%; 1396 | border-radius: 50%; 1397 | background-color: var(--color); 1398 | content: ""; 1399 | transition: margin 0.1s ease-in-out; 1400 | } 1401 | [type="checkbox"][role="switch"]:checked { 1402 | background-image: none; 1403 | } 1404 | [type="checkbox"][role="switch"]:checked::before { 1405 | margin-left: calc(1.125em - var(--border-width)); 1406 | -webkit-margin-start: calc(1.125em - var(--border-width)); 1407 | margin-inline-start: calc(1.125em - var(--border-width)); 1408 | } 1409 | [type="checkbox"]:checked[aria-invalid="false"], 1410 | [type="checkbox"][aria-invalid="false"], 1411 | [type="checkbox"][role="switch"]:checked[aria-invalid="false"], 1412 | [type="checkbox"][role="switch"][aria-invalid="false"], 1413 | [type="radio"]:checked[aria-invalid="false"], 1414 | [type="radio"][aria-invalid="false"] { 1415 | --border-color: var(--form-element-valid-border-color); 1416 | } 1417 | [type="checkbox"]:checked[aria-invalid="true"], 1418 | [type="checkbox"][aria-invalid="true"], 1419 | [type="checkbox"][role="switch"]:checked[aria-invalid="true"], 1420 | [type="checkbox"][role="switch"][aria-invalid="true"], 1421 | [type="radio"]:checked[aria-invalid="true"], 1422 | [type="radio"][aria-invalid="true"] { 1423 | --border-color: var(--form-element-invalid-border-color); 1424 | } 1425 | [type="color"]::-webkit-color-swatch-wrapper { 1426 | padding: 0; 1427 | } 1428 | [type="color"]::-moz-focus-inner { 1429 | padding: 0; 1430 | } 1431 | [type="color"]::-webkit-color-swatch { 1432 | border: 0; 1433 | border-radius: calc(var(--border-radius) * 0.5); 1434 | } 1435 | [type="color"]::-moz-color-swatch { 1436 | border: 0; 1437 | border-radius: calc(var(--border-radius) * 0.5); 1438 | } 1439 | input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"]):is( 1440 | [type="date"], 1441 | [type="datetime-local"], 1442 | [type="month"], 1443 | [type="time"], 1444 | [type="week"] 1445 | ) { 1446 | --icon-position: 0.75rem; 1447 | --icon-width: 1rem; 1448 | padding-right: calc(var(--icon-width) + var(--icon-position)); 1449 | background-image: var(--icon-date); 1450 | background-position: center right var(--icon-position); 1451 | background-size: var(--icon-width) auto; 1452 | background-repeat: no-repeat; 1453 | } 1454 | input:not( 1455 | [type="checkbox"], 1456 | [type="radio"], 1457 | [type="range"], 1458 | [type="file"] 1459 | )[type="time"] { 1460 | background-image: var(--icon-time); 1461 | } 1462 | [type="date"]::-webkit-calendar-picker-indicator, 1463 | [type="datetime-local"]::-webkit-calendar-picker-indicator, 1464 | [type="month"]::-webkit-calendar-picker-indicator, 1465 | [type="time"]::-webkit-calendar-picker-indicator, 1466 | [type="week"]::-webkit-calendar-picker-indicator { 1467 | width: var(--icon-width); 1468 | margin-right: calc(var(--icon-width) * -1); 1469 | margin-left: var(--icon-position); 1470 | opacity: 0; 1471 | } 1472 | [dir="rtl"] 1473 | :is( 1474 | [type="date"], 1475 | [type="datetime-local"], 1476 | [type="month"], 1477 | [type="time"], 1478 | [type="week"] 1479 | ) { 1480 | text-align: right; 1481 | } 1482 | @-moz-document url-prefix() { 1483 | [type="date"], 1484 | [type="datetime-local"], 1485 | [type="month"], 1486 | [type="time"], 1487 | [type="week"] { 1488 | padding-right: var(--form-element-spacing-horizontal) !important; 1489 | background-image: none !important; 1490 | } 1491 | } 1492 | [type="file"] { 1493 | --color: var(--muted-color); 1494 | padding: calc(var(--form-element-spacing-vertical) * 0.5) 0; 1495 | border: 0; 1496 | border-radius: 0; 1497 | background: 0 0; 1498 | } 1499 | [type="file"]::file-selector-button { 1500 | --background-color: var(--secondary); 1501 | --border-color: var(--secondary); 1502 | --color: var(--secondary-inverse); 1503 | margin-right: calc(var(--spacing) / 2); 1504 | margin-left: 0; 1505 | -webkit-margin-start: 0; 1506 | margin-inline-start: 0; 1507 | -webkit-margin-end: calc(var(--spacing) / 2); 1508 | margin-inline-end: calc(var(--spacing) / 2); 1509 | padding: calc(var(--form-element-spacing-vertical) * 0.5) 1510 | calc(var(--form-element-spacing-horizontal) * 0.5); 1511 | border: var(--border-width) solid var(--border-color); 1512 | border-radius: var(--border-radius); 1513 | outline: 0; 1514 | background-color: var(--background-color); 1515 | box-shadow: var(--box-shadow); 1516 | color: var(--color); 1517 | font-weight: var(--font-weight); 1518 | font-size: 1rem; 1519 | line-height: var(--line-height); 1520 | text-align: center; 1521 | cursor: pointer; 1522 | transition: 1523 | background-color var(--transition), 1524 | border-color var(--transition), 1525 | color var(--transition), 1526 | box-shadow var(--transition); 1527 | } 1528 | [type="file"]::file-selector-button:is(:hover, :active, :focus) { 1529 | --background-color: var(--secondary-hover); 1530 | --border-color: var(--secondary-hover); 1531 | } 1532 | [type="file"]::-webkit-file-upload-button { 1533 | --background-color: var(--secondary); 1534 | --border-color: var(--secondary); 1535 | --color: var(--secondary-inverse); 1536 | margin-right: calc(var(--spacing) / 2); 1537 | margin-left: 0; 1538 | -webkit-margin-start: 0; 1539 | margin-inline-start: 0; 1540 | -webkit-margin-end: calc(var(--spacing) / 2); 1541 | margin-inline-end: calc(var(--spacing) / 2); 1542 | padding: calc(var(--form-element-spacing-vertical) * 0.5) 1543 | calc(var(--form-element-spacing-horizontal) * 0.5); 1544 | border: var(--border-width) solid var(--border-color); 1545 | border-radius: var(--border-radius); 1546 | outline: 0; 1547 | background-color: var(--background-color); 1548 | box-shadow: var(--box-shadow); 1549 | color: var(--color); 1550 | font-weight: var(--font-weight); 1551 | font-size: 1rem; 1552 | line-height: var(--line-height); 1553 | text-align: center; 1554 | cursor: pointer; 1555 | -webkit-transition: 1556 | background-color var(--transition), 1557 | border-color var(--transition), 1558 | color var(--transition), 1559 | box-shadow var(--transition); 1560 | transition: 1561 | background-color var(--transition), 1562 | border-color var(--transition), 1563 | color var(--transition), 1564 | box-shadow var(--transition); 1565 | } 1566 | [type="file"]::-webkit-file-upload-button:is(:hover, :active, :focus) { 1567 | --background-color: var(--secondary-hover); 1568 | --border-color: var(--secondary-hover); 1569 | } 1570 | [type="file"]::-ms-browse { 1571 | --background-color: var(--secondary); 1572 | --border-color: var(--secondary); 1573 | --color: var(--secondary-inverse); 1574 | margin-right: calc(var(--spacing) / 2); 1575 | margin-left: 0; 1576 | margin-inline-start: 0; 1577 | margin-inline-end: calc(var(--spacing) / 2); 1578 | padding: calc(var(--form-element-spacing-vertical) * 0.5) 1579 | calc(var(--form-element-spacing-horizontal) * 0.5); 1580 | border: var(--border-width) solid var(--border-color); 1581 | border-radius: var(--border-radius); 1582 | outline: 0; 1583 | background-color: var(--background-color); 1584 | box-shadow: var(--box-shadow); 1585 | color: var(--color); 1586 | font-weight: var(--font-weight); 1587 | font-size: 1rem; 1588 | line-height: var(--line-height); 1589 | text-align: center; 1590 | cursor: pointer; 1591 | -ms-transition: 1592 | background-color var(--transition), 1593 | border-color var(--transition), 1594 | color var(--transition), 1595 | box-shadow var(--transition); 1596 | transition: 1597 | background-color var(--transition), 1598 | border-color var(--transition), 1599 | color var(--transition), 1600 | box-shadow var(--transition); 1601 | } 1602 | [type="file"]::-ms-browse:is(:hover, :active, :focus) { 1603 | --background-color: var(--secondary-hover); 1604 | --border-color: var(--secondary-hover); 1605 | } 1606 | [type="range"] { 1607 | -webkit-appearance: none; 1608 | -moz-appearance: none; 1609 | appearance: none; 1610 | width: 100%; 1611 | height: 1.25rem; 1612 | background: 0 0; 1613 | } 1614 | [type="range"]::-webkit-slider-runnable-track { 1615 | width: 100%; 1616 | height: 0.25rem; 1617 | border-radius: var(--border-radius); 1618 | background-color: var(--range-border-color); 1619 | -webkit-transition: 1620 | background-color var(--transition), 1621 | box-shadow var(--transition); 1622 | transition: 1623 | background-color var(--transition), 1624 | box-shadow var(--transition); 1625 | } 1626 | [type="range"]::-moz-range-track { 1627 | width: 100%; 1628 | height: 0.25rem; 1629 | border-radius: var(--border-radius); 1630 | background-color: var(--range-border-color); 1631 | -moz-transition: 1632 | background-color var(--transition), 1633 | box-shadow var(--transition); 1634 | transition: 1635 | background-color var(--transition), 1636 | box-shadow var(--transition); 1637 | } 1638 | [type="range"]::-ms-track { 1639 | width: 100%; 1640 | height: 0.25rem; 1641 | border-radius: var(--border-radius); 1642 | background-color: var(--range-border-color); 1643 | -ms-transition: 1644 | background-color var(--transition), 1645 | box-shadow var(--transition); 1646 | transition: 1647 | background-color var(--transition), 1648 | box-shadow var(--transition); 1649 | } 1650 | [type="range"]::-webkit-slider-thumb { 1651 | -webkit-appearance: none; 1652 | width: 1.25rem; 1653 | height: 1.25rem; 1654 | margin-top: -0.5rem; 1655 | border: 2px solid var(--range-thumb-border-color); 1656 | border-radius: 50%; 1657 | background-color: var(--range-thumb-color); 1658 | cursor: pointer; 1659 | -webkit-transition: 1660 | background-color var(--transition), 1661 | transform var(--transition); 1662 | transition: 1663 | background-color var(--transition), 1664 | transform var(--transition); 1665 | } 1666 | [type="range"]::-moz-range-thumb { 1667 | -webkit-appearance: none; 1668 | width: 1.25rem; 1669 | height: 1.25rem; 1670 | margin-top: -0.5rem; 1671 | border: 2px solid var(--range-thumb-border-color); 1672 | border-radius: 50%; 1673 | background-color: var(--range-thumb-color); 1674 | cursor: pointer; 1675 | -moz-transition: 1676 | background-color var(--transition), 1677 | transform var(--transition); 1678 | transition: 1679 | background-color var(--transition), 1680 | transform var(--transition); 1681 | } 1682 | [type="range"]::-ms-thumb { 1683 | -webkit-appearance: none; 1684 | width: 1.25rem; 1685 | height: 1.25rem; 1686 | margin-top: -0.5rem; 1687 | border: 2px solid var(--range-thumb-border-color); 1688 | border-radius: 50%; 1689 | background-color: var(--range-thumb-color); 1690 | cursor: pointer; 1691 | -ms-transition: 1692 | background-color var(--transition), 1693 | transform var(--transition); 1694 | transition: 1695 | background-color var(--transition), 1696 | transform var(--transition); 1697 | } 1698 | [type="range"]:focus, 1699 | [type="range"]:hover { 1700 | --range-border-color: var(--range-active-border-color); 1701 | --range-thumb-color: var(--range-thumb-hover-color); 1702 | } 1703 | [type="range"]:active { 1704 | --range-thumb-color: var(--range-thumb-active-color); 1705 | } 1706 | [type="range"]:active::-webkit-slider-thumb { 1707 | transform: scale(1.25); 1708 | } 1709 | [type="range"]:active::-moz-range-thumb { 1710 | transform: scale(1.25); 1711 | } 1712 | [type="range"]:active::-ms-thumb { 1713 | transform: scale(1.25); 1714 | } 1715 | input:not( 1716 | [type="checkbox"], 1717 | [type="radio"], 1718 | [type="range"], 1719 | [type="file"] 1720 | )[type="search"] { 1721 | -webkit-padding-start: calc(var(--form-element-spacing-horizontal) + 1.75rem); 1722 | padding-inline-start: calc(var(--form-element-spacing-horizontal) + 1.75rem); 1723 | border-radius: 5rem; 1724 | background-image: var(--icon-search); 1725 | background-position: center left 1.125rem; 1726 | background-size: 1rem auto; 1727 | background-repeat: no-repeat; 1728 | } 1729 | input:not( 1730 | [type="checkbox"], 1731 | [type="radio"], 1732 | [type="range"], 1733 | [type="file"] 1734 | )[type="search"][aria-invalid] { 1735 | -webkit-padding-start: calc( 1736 | var(--form-element-spacing-horizontal) + 1.75rem 1737 | ) !important; 1738 | padding-inline-start: calc( 1739 | var(--form-element-spacing-horizontal) + 1.75rem 1740 | ) !important; 1741 | background-position: 1742 | center left 1.125rem, 1743 | center right 0.75rem; 1744 | } 1745 | input:not( 1746 | [type="checkbox"], 1747 | [type="radio"], 1748 | [type="range"], 1749 | [type="file"] 1750 | )[type="search"][aria-invalid="false"] { 1751 | background-image: var(--icon-search), var(--icon-valid); 1752 | } 1753 | input:not( 1754 | [type="checkbox"], 1755 | [type="radio"], 1756 | [type="range"], 1757 | [type="file"] 1758 | )[type="search"][aria-invalid="true"] { 1759 | background-image: var(--icon-search), var(--icon-invalid); 1760 | } 1761 | [type="search"]::-webkit-search-cancel-button { 1762 | -webkit-appearance: none; 1763 | display: none; 1764 | } 1765 | [dir="rtl"] 1766 | :where(input):not( 1767 | [type="checkbox"], 1768 | [type="radio"], 1769 | [type="range"], 1770 | [type="file"] 1771 | )[type="search"] { 1772 | background-position: center right 1.125rem; 1773 | } 1774 | [dir="rtl"] 1775 | :where(input):not( 1776 | [type="checkbox"], 1777 | [type="radio"], 1778 | [type="range"], 1779 | [type="file"] 1780 | )[type="search"][aria-invalid] { 1781 | background-position: 1782 | center right 1.125rem, 1783 | center left 0.75rem; 1784 | } 1785 | :where(table) { 1786 | width: 100%; 1787 | border-collapse: collapse; 1788 | border-spacing: 0; 1789 | text-indent: 0; 1790 | } 1791 | td, 1792 | th { 1793 | padding: calc(var(--spacing) / 2) var(--spacing); 1794 | border-bottom: var(--border-width) solid var(--table-border-color); 1795 | color: var(--color); 1796 | font-weight: var(--font-weight); 1797 | font-size: var(--font-size); 1798 | text-align: left; 1799 | text-align: start; 1800 | } 1801 | tfoot td, 1802 | tfoot th { 1803 | border-top: var(--border-width) solid var(--table-border-color); 1804 | border-bottom: 0; 1805 | } 1806 | table[role="grid"] tbody tr:nth-child(odd) { 1807 | background-color: var(--table-row-stripped-background-color); 1808 | } 1809 | code, 1810 | kbd, 1811 | pre, 1812 | samp { 1813 | font-size: 0.875em; 1814 | font-family: var(--font-family); 1815 | } 1816 | pre { 1817 | -ms-overflow-style: scrollbar; 1818 | overflow: auto; 1819 | } 1820 | code, 1821 | kbd, 1822 | pre { 1823 | border-radius: var(--border-radius); 1824 | background: var(--code-background-color); 1825 | color: var(--code-color); 1826 | font-weight: var(--font-weight); 1827 | line-height: initial; 1828 | } 1829 | code, 1830 | kbd { 1831 | display: inline-block; 1832 | padding: 0.375rem 0.5rem; 1833 | } 1834 | pre { 1835 | display: block; 1836 | margin-bottom: var(--spacing); 1837 | overflow-x: auto; 1838 | } 1839 | pre > code { 1840 | display: block; 1841 | padding: var(--spacing); 1842 | background: 0 0; 1843 | font-size: 14px; 1844 | line-height: var(--line-height); 1845 | } 1846 | code b { 1847 | color: var(--code-tag-color); 1848 | font-weight: var(--font-weight); 1849 | } 1850 | code i { 1851 | color: var(--code-property-color); 1852 | font-style: normal; 1853 | } 1854 | code u { 1855 | color: var(--code-value-color); 1856 | text-decoration: none; 1857 | } 1858 | code em { 1859 | color: var(--code-comment-color); 1860 | font-style: normal; 1861 | } 1862 | kbd { 1863 | background-color: var(--code-kbd-background-color); 1864 | color: var(--code-kbd-color); 1865 | vertical-align: baseline; 1866 | } 1867 | hr { 1868 | height: 0; 1869 | border: 0; 1870 | border-top: 1px solid var(--muted-border-color); 1871 | color: inherit; 1872 | } 1873 | [hidden], 1874 | template { 1875 | display: none !important; 1876 | } 1877 | canvas { 1878 | display: inline-block; 1879 | } 1880 | details { 1881 | display: block; 1882 | margin-bottom: var(--spacing); 1883 | padding-bottom: var(--spacing); 1884 | border-bottom: var(--border-width) solid var(--accordion-border-color); 1885 | } 1886 | details summary { 1887 | line-height: 1rem; 1888 | list-style-type: none; 1889 | cursor: pointer; 1890 | transition: color var(--transition); 1891 | } 1892 | details summary:not([role]) { 1893 | color: var(--accordion-close-summary-color); 1894 | } 1895 | details summary::-webkit-details-marker { 1896 | display: none; 1897 | } 1898 | details summary::marker { 1899 | display: none; 1900 | } 1901 | details summary::-moz-list-bullet { 1902 | list-style-type: none; 1903 | } 1904 | details summary::after { 1905 | display: block; 1906 | width: 1rem; 1907 | height: 1rem; 1908 | -webkit-margin-start: calc(var(--spacing, 1rem) * 0.5); 1909 | margin-inline-start: calc(var(--spacing, 1rem) * 0.5); 1910 | float: right; 1911 | transform: rotate(-90deg); 1912 | background-image: var(--icon-chevron); 1913 | background-position: right center; 1914 | background-size: 1rem auto; 1915 | background-repeat: no-repeat; 1916 | content: ""; 1917 | transition: transform var(--transition); 1918 | } 1919 | details summary:focus { 1920 | outline: 0; 1921 | } 1922 | details summary:focus:not([role="button"]) { 1923 | color: var(--accordion-active-summary-color); 1924 | } 1925 | details summary[role="button"] { 1926 | width: 100%; 1927 | text-align: left; 1928 | } 1929 | details summary[role="button"]::after { 1930 | height: calc(1rem * var(--line-height, 1.5)); 1931 | background-image: var(--icon-chevron-button); 1932 | } 1933 | details summary[role="button"]:not(.outline).contrast::after { 1934 | background-image: var(--icon-chevron-button-inverse); 1935 | } 1936 | details[open] > summary { 1937 | margin-bottom: calc(var(--spacing)); 1938 | } 1939 | details[open] > summary:not([role]):not(:focus) { 1940 | color: var(--accordion-open-summary-color); 1941 | } 1942 | details[open] > summary::after { 1943 | transform: rotate(0); 1944 | } 1945 | [dir="rtl"] details summary { 1946 | text-align: right; 1947 | } 1948 | [dir="rtl"] details summary::after { 1949 | float: left; 1950 | background-position: left center; 1951 | } 1952 | article { 1953 | margin: var(--block-spacing-vertical) 0; 1954 | padding: var(--block-spacing-vertical) var(--block-spacing-horizontal); 1955 | border-radius: var(--border-radius); 1956 | background: var(--card-background-color); 1957 | box-shadow: var(--card-box-shadow); 1958 | } 1959 | article > footer, 1960 | article > header { 1961 | margin-right: calc(var(--block-spacing-horizontal) * -1); 1962 | margin-left: calc(var(--block-spacing-horizontal) * -1); 1963 | padding: calc(var(--block-spacing-vertical) * 0.66) 1964 | var(--block-spacing-horizontal); 1965 | background-color: var(--card-sectionning-background-color); 1966 | } 1967 | article > header { 1968 | margin-top: calc(var(--block-spacing-vertical) * -1); 1969 | margin-bottom: var(--block-spacing-vertical); 1970 | border-bottom: var(--border-width) solid var(--card-border-color); 1971 | border-top-right-radius: var(--border-radius); 1972 | border-top-left-radius: var(--border-radius); 1973 | } 1974 | article > footer { 1975 | margin-top: var(--block-spacing-vertical); 1976 | margin-bottom: calc(var(--block-spacing-vertical) * -1); 1977 | border-top: var(--border-width) solid var(--card-border-color); 1978 | border-bottom-right-radius: var(--border-radius); 1979 | border-bottom-left-radius: var(--border-radius); 1980 | } 1981 | :root { 1982 | --scrollbar-width: 0px; 1983 | } 1984 | dialog { 1985 | display: flex; 1986 | z-index: 999; 1987 | position: fixed; 1988 | top: 0; 1989 | right: 0; 1990 | bottom: 0; 1991 | left: 0; 1992 | align-items: center; 1993 | justify-content: center; 1994 | width: inherit; 1995 | min-width: 100%; 1996 | height: inherit; 1997 | min-height: 100%; 1998 | padding: var(--spacing); 1999 | border: 0; 2000 | -webkit-backdrop-filter: var(--modal-overlay-backdrop-filter); 2001 | backdrop-filter: var(--modal-overlay-backdrop-filter); 2002 | background-color: var(--modal-overlay-background-color); 2003 | color: var(--color); 2004 | } 2005 | dialog article { 2006 | max-height: calc(100vh - var(--spacing) * 2); 2007 | overflow: auto; 2008 | } 2009 | @media (min-width: 576px) { 2010 | dialog article { 2011 | max-width: 510px; 2012 | } 2013 | } 2014 | @media (min-width: 768px) { 2015 | dialog article { 2016 | max-width: 700px; 2017 | } 2018 | } 2019 | dialog article > footer, 2020 | dialog article > header { 2021 | padding: calc(var(--block-spacing-vertical) * 0.5) 2022 | var(--block-spacing-horizontal); 2023 | } 2024 | dialog article > header .close { 2025 | margin: 0; 2026 | margin-left: var(--spacing); 2027 | float: right; 2028 | } 2029 | dialog article > footer { 2030 | text-align: right; 2031 | } 2032 | dialog article > footer [role="button"] { 2033 | margin-bottom: 0; 2034 | } 2035 | dialog article > footer [role="button"]:not(:first-of-type) { 2036 | margin-left: calc(var(--spacing) * 0.5); 2037 | } 2038 | dialog article p:last-of-type { 2039 | margin: 0; 2040 | } 2041 | dialog article .close { 2042 | display: block; 2043 | width: 1rem; 2044 | height: 1rem; 2045 | margin-top: calc(var(--block-spacing-vertical) * -0.5); 2046 | margin-bottom: var(--typography-spacing-vertical); 2047 | margin-left: auto; 2048 | background-image: var(--icon-close); 2049 | background-position: center; 2050 | background-size: auto 1rem; 2051 | background-repeat: no-repeat; 2052 | opacity: 0.5; 2053 | transition: opacity var(--transition); 2054 | } 2055 | dialog article .close:is([aria-current], :hover, :active, :focus) { 2056 | opacity: 1; 2057 | } 2058 | dialog:not([open]), 2059 | dialog[open="false"] { 2060 | display: none; 2061 | } 2062 | .modal-is-open { 2063 | padding-right: var(--scrollbar-width, 0); 2064 | overflow: hidden; 2065 | pointer-events: none; 2066 | touch-action: none; 2067 | } 2068 | .modal-is-open dialog { 2069 | pointer-events: auto; 2070 | } 2071 | :where(.modal-is-opening, .modal-is-closing) dialog, 2072 | :where(.modal-is-opening, .modal-is-closing) dialog > article { 2073 | animation-duration: 0.2s; 2074 | animation-timing-function: ease-in-out; 2075 | animation-fill-mode: both; 2076 | } 2077 | :where(.modal-is-opening, .modal-is-closing) dialog { 2078 | animation-duration: 0.8s; 2079 | animation-name: modal-overlay; 2080 | } 2081 | :where(.modal-is-opening, .modal-is-closing) dialog > article { 2082 | animation-delay: 0.2s; 2083 | animation-name: modal; 2084 | } 2085 | .modal-is-closing dialog, 2086 | .modal-is-closing dialog > article { 2087 | animation-delay: 0s; 2088 | animation-direction: reverse; 2089 | } 2090 | @keyframes modal-overlay { 2091 | from { 2092 | -webkit-backdrop-filter: none; 2093 | backdrop-filter: none; 2094 | background-color: transparent; 2095 | } 2096 | } 2097 | @keyframes modal { 2098 | from { 2099 | transform: translateY(-100%); 2100 | opacity: 0; 2101 | } 2102 | } 2103 | :where(nav li)::before { 2104 | float: left; 2105 | content: "​"; 2106 | } 2107 | nav, 2108 | nav ul { 2109 | display: flex; 2110 | } 2111 | nav { 2112 | justify-content: space-between; 2113 | } 2114 | nav ol, 2115 | nav ul { 2116 | align-items: center; 2117 | margin-bottom: 0; 2118 | padding: 0; 2119 | list-style: none; 2120 | } 2121 | nav ol:first-of-type, 2122 | nav ul:first-of-type { 2123 | margin-left: calc(var(--nav-element-spacing-horizontal) * -1); 2124 | } 2125 | nav ol:last-of-type, 2126 | nav ul:last-of-type { 2127 | margin-right: calc(var(--nav-element-spacing-horizontal) * -1); 2128 | } 2129 | nav li { 2130 | display: inline-block; 2131 | margin: 0; 2132 | padding: var(--nav-element-spacing-vertical) 2133 | var(--nav-element-spacing-horizontal); 2134 | } 2135 | nav li > * { 2136 | --spacing: 0; 2137 | } 2138 | nav :where(a, [role="link"]) { 2139 | display: inline-block; 2140 | margin: calc(var(--nav-link-spacing-vertical) * -1) 2141 | calc(var(--nav-link-spacing-horizontal) * -1); 2142 | padding: var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal); 2143 | border-radius: var(--border-radius); 2144 | text-decoration: none; 2145 | } 2146 | nav :where(a, [role="link"]):is([aria-current], :hover, :active, :focus) { 2147 | text-decoration: none; 2148 | } 2149 | nav[aria-label="breadcrumb"] { 2150 | align-items: center; 2151 | justify-content: start; 2152 | } 2153 | nav[aria-label="breadcrumb"] ul li:not(:first-child) { 2154 | -webkit-margin-start: var(--nav-link-spacing-horizontal); 2155 | margin-inline-start: var(--nav-link-spacing-horizontal); 2156 | } 2157 | nav[aria-label="breadcrumb"] ul li:not(:last-child) ::after { 2158 | position: absolute; 2159 | width: calc(var(--nav-link-spacing-horizontal) * 2); 2160 | -webkit-margin-start: calc(var(--nav-link-spacing-horizontal) / 2); 2161 | margin-inline-start: calc(var(--nav-link-spacing-horizontal) / 2); 2162 | content: "/"; 2163 | color: var(--muted-color); 2164 | text-align: center; 2165 | } 2166 | nav[aria-label="breadcrumb"] a[aria-current] { 2167 | background-color: transparent; 2168 | color: inherit; 2169 | text-decoration: none; 2170 | pointer-events: none; 2171 | } 2172 | nav [role="button"] { 2173 | margin-right: inherit; 2174 | margin-left: inherit; 2175 | padding: var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal); 2176 | } 2177 | aside li, 2178 | aside nav, 2179 | aside ol, 2180 | aside ul { 2181 | display: block; 2182 | } 2183 | aside li { 2184 | padding: calc(var(--nav-element-spacing-vertical) * 0.5) 2185 | var(--nav-element-spacing-horizontal); 2186 | } 2187 | aside li a { 2188 | display: block; 2189 | } 2190 | aside li [role="button"] { 2191 | margin: inherit; 2192 | } 2193 | [dir="rtl"] nav[aria-label="breadcrumb"] ul li:not(:last-child) ::after { 2194 | content: "\\"; 2195 | } 2196 | progress { 2197 | display: inline-block; 2198 | vertical-align: baseline; 2199 | } 2200 | progress { 2201 | -webkit-appearance: none; 2202 | -moz-appearance: none; 2203 | display: inline-block; 2204 | appearance: none; 2205 | width: 100%; 2206 | height: 0.5rem; 2207 | margin-bottom: calc(var(--spacing) * 0.5); 2208 | overflow: hidden; 2209 | border: 0; 2210 | border-radius: var(--border-radius); 2211 | background-color: var(--progress-background-color); 2212 | color: var(--progress-color); 2213 | } 2214 | progress::-webkit-progress-bar { 2215 | border-radius: var(--border-radius); 2216 | background: 0 0; 2217 | } 2218 | progress[value]::-webkit-progress-value { 2219 | background-color: var(--progress-color); 2220 | } 2221 | progress::-moz-progress-bar { 2222 | background-color: var(--progress-color); 2223 | } 2224 | @media (prefers-reduced-motion: no-preference) { 2225 | progress:indeterminate { 2226 | background: var(--progress-background-color) 2227 | linear-gradient( 2228 | to right, 2229 | var(--progress-color) 30%, 2230 | var(--progress-background-color) 30% 2231 | ) 2232 | top left/150% 150% no-repeat; 2233 | animation: progress-indeterminate 1s linear infinite; 2234 | } 2235 | progress:indeterminate[value]::-webkit-progress-value { 2236 | background-color: transparent; 2237 | } 2238 | progress:indeterminate::-moz-progress-bar { 2239 | background-color: transparent; 2240 | } 2241 | } 2242 | @media (prefers-reduced-motion: no-preference) { 2243 | [dir="rtl"] progress:indeterminate { 2244 | animation-direction: reverse; 2245 | } 2246 | } 2247 | @keyframes progress-indeterminate { 2248 | 0% { 2249 | background-position: 200% 0; 2250 | } 2251 | 100% { 2252 | background-position: -200% 0; 2253 | } 2254 | } 2255 | details[role="list"], 2256 | li[role="list"] { 2257 | position: relative; 2258 | } 2259 | details[role="list"] summary + ul, 2260 | li[role="list"] > ul { 2261 | display: flex; 2262 | z-index: 99; 2263 | position: absolute; 2264 | top: auto; 2265 | right: 0; 2266 | left: 0; 2267 | flex-direction: column; 2268 | margin: 0; 2269 | padding: 0; 2270 | border: var(--border-width) solid var(--dropdown-border-color); 2271 | border-radius: var(--border-radius); 2272 | border-top-right-radius: 0; 2273 | border-top-left-radius: 0; 2274 | background-color: var(--dropdown-background-color); 2275 | box-shadow: var(--card-box-shadow); 2276 | color: var(--dropdown-color); 2277 | white-space: nowrap; 2278 | } 2279 | details[role="list"] summary + ul li, 2280 | li[role="list"] > ul li { 2281 | width: 100%; 2282 | margin-bottom: 0; 2283 | padding: calc(var(--form-element-spacing-vertical) * 0.5) 2284 | var(--form-element-spacing-horizontal); 2285 | list-style: none; 2286 | } 2287 | details[role="list"] summary + ul li:first-of-type, 2288 | li[role="list"] > ul li:first-of-type { 2289 | margin-top: calc(var(--form-element-spacing-vertical) * 0.5); 2290 | } 2291 | details[role="list"] summary + ul li:last-of-type, 2292 | li[role="list"] > ul li:last-of-type { 2293 | margin-bottom: calc(var(--form-element-spacing-vertical) * 0.5); 2294 | } 2295 | details[role="list"] summary + ul li a, 2296 | li[role="list"] > ul li a { 2297 | display: block; 2298 | margin: calc(var(--form-element-spacing-vertical) * -0.5) 2299 | calc(var(--form-element-spacing-horizontal) * -1); 2300 | padding: calc(var(--form-element-spacing-vertical) * 0.5) 2301 | var(--form-element-spacing-horizontal); 2302 | overflow: hidden; 2303 | color: var(--dropdown-color); 2304 | text-decoration: none; 2305 | text-overflow: ellipsis; 2306 | } 2307 | details[role="list"] summary + ul li a:hover, 2308 | li[role="list"] > ul li a:hover { 2309 | background-color: var(--dropdown-hover-background-color); 2310 | } 2311 | details[role="list"] summary::after, 2312 | li[role="list"] > a::after { 2313 | display: block; 2314 | width: 1rem; 2315 | height: calc(1rem * var(--line-height, 1.5)); 2316 | -webkit-margin-start: 0.5rem; 2317 | margin-inline-start: 0.5rem; 2318 | float: right; 2319 | transform: rotate(0); 2320 | background-position: right center; 2321 | background-size: 1rem auto; 2322 | background-repeat: no-repeat; 2323 | content: ""; 2324 | } 2325 | details[role="list"] { 2326 | padding: 0; 2327 | border-bottom: none; 2328 | } 2329 | details[role="list"] summary { 2330 | margin-bottom: 0; 2331 | } 2332 | details[role="list"] summary:not([role]) { 2333 | height: calc( 2334 | 1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + 2335 | var(--border-width) * 2 2336 | ); 2337 | padding: var(--form-element-spacing-vertical) 2338 | var(--form-element-spacing-horizontal); 2339 | border: var(--border-width) solid var(--form-element-border-color); 2340 | border-radius: var(--border-radius); 2341 | background-color: var(--form-element-background-color); 2342 | color: var(--form-element-placeholder-color); 2343 | line-height: inherit; 2344 | cursor: pointer; 2345 | transition: 2346 | background-color var(--transition), 2347 | border-color var(--transition), 2348 | color var(--transition), 2349 | box-shadow var(--transition); 2350 | } 2351 | details[role="list"] summary:not([role]):active, 2352 | details[role="list"] summary:not([role]):focus { 2353 | border-color: var(--form-element-active-border-color); 2354 | background-color: var(--form-element-active-background-color); 2355 | } 2356 | details[role="list"] summary:not([role]):focus { 2357 | box-shadow: 0 0 0 var(--outline-width) var(--form-element-focus-color); 2358 | } 2359 | details[role="list"][open] summary { 2360 | border-bottom-right-radius: 0; 2361 | border-bottom-left-radius: 0; 2362 | } 2363 | details[role="list"][open] summary::before { 2364 | display: block; 2365 | z-index: 1; 2366 | position: fixed; 2367 | top: 0; 2368 | right: 0; 2369 | bottom: 0; 2370 | left: 0; 2371 | background: 0 0; 2372 | content: ""; 2373 | cursor: default; 2374 | } 2375 | nav details[role="list"] summary, 2376 | nav li[role="list"] a { 2377 | display: flex; 2378 | direction: ltr; 2379 | } 2380 | nav details[role="list"] summary + ul, 2381 | nav li[role="list"] > ul { 2382 | min-width: -moz-fit-content; 2383 | min-width: fit-content; 2384 | border-radius: var(--border-radius); 2385 | } 2386 | nav details[role="list"] summary + ul li a, 2387 | nav li[role="list"] > ul li a { 2388 | border-radius: 0; 2389 | } 2390 | nav details[role="list"] summary, 2391 | nav details[role="list"] summary:not([role]) { 2392 | height: auto; 2393 | padding: var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal); 2394 | } 2395 | nav details[role="list"][open] summary { 2396 | border-radius: var(--border-radius); 2397 | } 2398 | nav details[role="list"] summary + ul { 2399 | margin-top: var(--outline-width); 2400 | -webkit-margin-start: 0; 2401 | margin-inline-start: 0; 2402 | } 2403 | nav details[role="list"] summary[role="link"] { 2404 | margin-bottom: calc(var(--nav-link-spacing-vertical) * -1); 2405 | line-height: var(--line-height); 2406 | } 2407 | nav details[role="list"] summary[role="link"] + ul { 2408 | margin-top: calc(var(--nav-link-spacing-vertical) + var(--outline-width)); 2409 | -webkit-margin-start: calc(var(--nav-link-spacing-horizontal) * -1); 2410 | margin-inline-start: calc(var(--nav-link-spacing-horizontal) * -1); 2411 | } 2412 | li[role="list"] a:active ~ ul, 2413 | li[role="list"] a:focus ~ ul, 2414 | li[role="list"]:hover > ul { 2415 | display: flex; 2416 | } 2417 | li[role="list"] > ul { 2418 | display: none; 2419 | margin-top: calc(var(--nav-link-spacing-vertical) + var(--outline-width)); 2420 | -webkit-margin-start: calc( 2421 | var(--nav-element-spacing-horizontal) - var(--nav-link-spacing-horizontal) 2422 | ); 2423 | margin-inline-start: calc( 2424 | var(--nav-element-spacing-horizontal) - var(--nav-link-spacing-horizontal) 2425 | ); 2426 | } 2427 | li[role="list"] > a::after { 2428 | background-image: var(--icon-chevron); 2429 | } 2430 | label > details[role="list"] { 2431 | margin-top: calc(var(--spacing) * 0.25); 2432 | margin-bottom: var(--spacing); 2433 | } 2434 | [aria-busy="true"] { 2435 | cursor: progress; 2436 | } 2437 | [aria-busy="true"]:not(input, select, textarea, html)::before { 2438 | display: inline-block; 2439 | width: 1em; 2440 | height: 1em; 2441 | border: 0.1875em solid currentColor; 2442 | border-radius: 1em; 2443 | border-right-color: transparent; 2444 | content: ""; 2445 | vertical-align: text-bottom; 2446 | vertical-align: -0.125em; 2447 | animation: spinner 0.75s linear infinite; 2448 | opacity: var(--loading-spinner-opacity); 2449 | } 2450 | [aria-busy="true"]:not(input, select, textarea, html):not(:empty)::before { 2451 | margin-right: calc(var(--spacing) * 0.5); 2452 | margin-left: 0; 2453 | -webkit-margin-start: 0; 2454 | margin-inline-start: 0; 2455 | -webkit-margin-end: calc(var(--spacing) * 0.5); 2456 | margin-inline-end: calc(var(--spacing) * 0.5); 2457 | } 2458 | [aria-busy="true"]:not(input, select, textarea, html):empty { 2459 | text-align: center; 2460 | } 2461 | a[aria-busy="true"], 2462 | button[aria-busy="true"], 2463 | input[type="button"][aria-busy="true"], 2464 | input[type="reset"][aria-busy="true"], 2465 | input[type="submit"][aria-busy="true"] { 2466 | pointer-events: none; 2467 | } 2468 | @keyframes spinner { 2469 | to { 2470 | transform: rotate(360deg); 2471 | } 2472 | } 2473 | [data-tooltip] { 2474 | position: relative; 2475 | } 2476 | [data-tooltip]:not(a, button, input) { 2477 | border-bottom: 1px dotted; 2478 | text-decoration: none; 2479 | cursor: help; 2480 | } 2481 | [data-tooltip]::after, 2482 | [data-tooltip]::before, 2483 | [data-tooltip][data-placement="top"]::after, 2484 | [data-tooltip][data-placement="top"]::before { 2485 | display: block; 2486 | z-index: 99; 2487 | position: absolute; 2488 | bottom: 100%; 2489 | left: 50%; 2490 | padding: 0.25rem 0.5rem; 2491 | overflow: hidden; 2492 | transform: translate(-50%, -0.25rem); 2493 | border-radius: var(--border-radius); 2494 | background: var(--tooltip-background-color); 2495 | content: attr(data-tooltip); 2496 | color: var(--tooltip-color); 2497 | font-style: normal; 2498 | font-weight: var(--font-weight); 2499 | font-size: 0.875rem; 2500 | text-decoration: none; 2501 | text-overflow: ellipsis; 2502 | white-space: nowrap; 2503 | opacity: 0; 2504 | pointer-events: none; 2505 | } 2506 | [data-tooltip]::after, 2507 | [data-tooltip][data-placement="top"]::after { 2508 | padding: 0; 2509 | transform: translate(-50%, 0); 2510 | border-top: 0.3rem solid; 2511 | border-right: 0.3rem solid transparent; 2512 | border-left: 0.3rem solid transparent; 2513 | border-radius: 0; 2514 | background-color: transparent; 2515 | content: ""; 2516 | color: var(--tooltip-background-color); 2517 | } 2518 | [data-tooltip][data-placement="bottom"]::after, 2519 | [data-tooltip][data-placement="bottom"]::before { 2520 | top: 100%; 2521 | bottom: auto; 2522 | transform: translate(-50%, 0.25rem); 2523 | } 2524 | [data-tooltip][data-placement="bottom"]:after { 2525 | transform: translate(-50%, -0.3rem); 2526 | border: 0.3rem solid transparent; 2527 | border-bottom: 0.3rem solid; 2528 | } 2529 | [data-tooltip][data-placement="left"]::after, 2530 | [data-tooltip][data-placement="left"]::before { 2531 | top: 50%; 2532 | right: 100%; 2533 | bottom: auto; 2534 | left: auto; 2535 | transform: translate(-0.25rem, -50%); 2536 | } 2537 | [data-tooltip][data-placement="left"]:after { 2538 | transform: translate(0.3rem, -50%); 2539 | border: 0.3rem solid transparent; 2540 | border-left: 0.3rem solid; 2541 | } 2542 | [data-tooltip][data-placement="right"]::after, 2543 | [data-tooltip][data-placement="right"]::before { 2544 | top: 50%; 2545 | right: auto; 2546 | bottom: auto; 2547 | left: 100%; 2548 | transform: translate(0.25rem, -50%); 2549 | } 2550 | [data-tooltip][data-placement="right"]:after { 2551 | transform: translate(-0.3rem, -50%); 2552 | border: 0.3rem solid transparent; 2553 | border-right: 0.3rem solid; 2554 | } 2555 | [data-tooltip]:focus::after, 2556 | [data-tooltip]:focus::before, 2557 | [data-tooltip]:hover::after, 2558 | [data-tooltip]:hover::before { 2559 | opacity: 1; 2560 | } 2561 | @media (hover: hover) and (pointer: fine) { 2562 | [data-tooltip]:hover::after, 2563 | [data-tooltip]:hover::before, 2564 | [data-tooltip][data-placement="bottom"]:focus::after, 2565 | [data-tooltip][data-placement="bottom"]:focus::before, 2566 | [data-tooltip][data-placement="bottom"]:hover [data-tooltip]:focus::after, 2567 | [data-tooltip][data-placement="bottom"]:hover [data-tooltip]:focus::before { 2568 | animation-duration: 0.2s; 2569 | animation-name: tooltip-slide-top; 2570 | } 2571 | [data-tooltip]:hover::after, 2572 | [data-tooltip][data-placement="bottom"]:focus::after, 2573 | [data-tooltip][data-placement="bottom"]:hover [data-tooltip]:focus::after { 2574 | animation-name: tooltip-caret-slide-top; 2575 | } 2576 | [data-tooltip][data-placement="bottom"]:focus::after, 2577 | [data-tooltip][data-placement="bottom"]:focus::before, 2578 | [data-tooltip][data-placement="bottom"]:hover::after, 2579 | [data-tooltip][data-placement="bottom"]:hover::before { 2580 | animation-duration: 0.2s; 2581 | animation-name: tooltip-slide-bottom; 2582 | } 2583 | [data-tooltip][data-placement="bottom"]:focus::after, 2584 | [data-tooltip][data-placement="bottom"]:hover::after { 2585 | animation-name: tooltip-caret-slide-bottom; 2586 | } 2587 | [data-tooltip][data-placement="left"]:focus::after, 2588 | [data-tooltip][data-placement="left"]:focus::before, 2589 | [data-tooltip][data-placement="left"]:hover::after, 2590 | [data-tooltip][data-placement="left"]:hover::before { 2591 | animation-duration: 0.2s; 2592 | animation-name: tooltip-slide-left; 2593 | } 2594 | [data-tooltip][data-placement="left"]:focus::after, 2595 | [data-tooltip][data-placement="left"]:hover::after { 2596 | animation-name: tooltip-caret-slide-left; 2597 | } 2598 | [data-tooltip][data-placement="right"]:focus::after, 2599 | [data-tooltip][data-placement="right"]:focus::before, 2600 | [data-tooltip][data-placement="right"]:hover::after, 2601 | [data-tooltip][data-placement="right"]:hover::before { 2602 | animation-duration: 0.2s; 2603 | animation-name: tooltip-slide-right; 2604 | } 2605 | [data-tooltip][data-placement="right"]:focus::after, 2606 | [data-tooltip][data-placement="right"]:hover::after { 2607 | animation-name: tooltip-caret-slide-right; 2608 | } 2609 | } 2610 | @keyframes tooltip-slide-top { 2611 | from { 2612 | transform: translate(-50%, 0.75rem); 2613 | opacity: 0; 2614 | } 2615 | to { 2616 | transform: translate(-50%, -0.25rem); 2617 | opacity: 1; 2618 | } 2619 | } 2620 | @keyframes tooltip-caret-slide-top { 2621 | from { 2622 | opacity: 0; 2623 | } 2624 | 50% { 2625 | transform: translate(-50%, -0.25rem); 2626 | opacity: 0; 2627 | } 2628 | to { 2629 | transform: translate(-50%, 0); 2630 | opacity: 1; 2631 | } 2632 | } 2633 | @keyframes tooltip-slide-bottom { 2634 | from { 2635 | transform: translate(-50%, -0.75rem); 2636 | opacity: 0; 2637 | } 2638 | to { 2639 | transform: translate(-50%, 0.25rem); 2640 | opacity: 1; 2641 | } 2642 | } 2643 | @keyframes tooltip-caret-slide-bottom { 2644 | from { 2645 | opacity: 0; 2646 | } 2647 | 50% { 2648 | transform: translate(-50%, -0.5rem); 2649 | opacity: 0; 2650 | } 2651 | to { 2652 | transform: translate(-50%, -0.3rem); 2653 | opacity: 1; 2654 | } 2655 | } 2656 | @keyframes tooltip-slide-left { 2657 | from { 2658 | transform: translate(0.75rem, -50%); 2659 | opacity: 0; 2660 | } 2661 | to { 2662 | transform: translate(-0.25rem, -50%); 2663 | opacity: 1; 2664 | } 2665 | } 2666 | @keyframes tooltip-caret-slide-left { 2667 | from { 2668 | opacity: 0; 2669 | } 2670 | 50% { 2671 | transform: translate(0.05rem, -50%); 2672 | opacity: 0; 2673 | } 2674 | to { 2675 | transform: translate(0.3rem, -50%); 2676 | opacity: 1; 2677 | } 2678 | } 2679 | @keyframes tooltip-slide-right { 2680 | from { 2681 | transform: translate(-0.75rem, -50%); 2682 | opacity: 0; 2683 | } 2684 | to { 2685 | transform: translate(0.25rem, -50%); 2686 | opacity: 1; 2687 | } 2688 | } 2689 | @keyframes tooltip-caret-slide-right { 2690 | from { 2691 | opacity: 0; 2692 | } 2693 | 50% { 2694 | transform: translate(-0.05rem, -50%); 2695 | opacity: 0; 2696 | } 2697 | to { 2698 | transform: translate(-0.3rem, -50%); 2699 | opacity: 1; 2700 | } 2701 | } 2702 | [aria-controls] { 2703 | cursor: pointer; 2704 | } 2705 | [aria-disabled="true"], 2706 | [disabled] { 2707 | cursor: not-allowed; 2708 | } 2709 | [aria-hidden="false"][hidden] { 2710 | display: initial; 2711 | } 2712 | [aria-hidden="false"][hidden]:not(:focus) { 2713 | clip: rect(0, 0, 0, 0); 2714 | position: absolute; 2715 | } 2716 | [tabindex], 2717 | a, 2718 | area, 2719 | button, 2720 | input, 2721 | label, 2722 | select, 2723 | summary, 2724 | textarea { 2725 | -ms-touch-action: manipulation; 2726 | } 2727 | [dir="rtl"] { 2728 | direction: rtl; 2729 | } 2730 | @media (prefers-reduced-motion: reduce) { 2731 | :not([aria-busy="true"]), 2732 | :not([aria-busy="true"])::after, 2733 | :not([aria-busy="true"])::before { 2734 | background-attachment: initial !important; 2735 | animation-duration: 1ms !important; 2736 | animation-delay: -1ms !important; 2737 | animation-iteration-count: 1 !important; 2738 | scroll-behavior: auto !important; 2739 | transition-delay: 0s !important; 2740 | transition-duration: 0s !important; 2741 | } 2742 | } 2743 | /*# sourceMappingURL=pico.min.css.map */ 2744 | --------------------------------------------------------------------------------