├── .dockerignore
├── .gitignore
├── public
└── favicon.ico
├── start_with_migrations.sh
├── remix.env.d.ts
├── app
├── entry.client.tsx
├── routes
│ ├── profile.tsx
│ ├── index.tsx
│ ├── logout.tsx
│ ├── api
│ │ └── on_item_insert.ts
│ ├── profile
│ │ └── index.tsx
│ └── login.tsx
├── entry.server.tsx
├── root.tsx
├── utils
│ ├── session.server.ts
│ └── hasura.server.ts
└── styles
│ └── tailwind.css
├── tailwind.config.js
├── remix.config.js
├── tsconfig.json
├── fly.toml
├── README.md
├── package.json
└── Dockerfile
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | /public/build
6 | .env
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nyoung697/remix-hasura-fly/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/start_with_migrations.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -ex
4 | npx prisma migrate deploy
5 | npm run start
6 |
--------------------------------------------------------------------------------
/remix.env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | import { hydrate } from "react-dom";
2 | import { RemixBrowser } from "remix";
3 |
4 | hydrate(, document);
5 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ["./app/**/*.{ts,tsx}"],
3 | theme: {
4 | extend: {},
5 | },
6 | plugins: [require("@tailwindcss/forms")],
7 | };
8 |
--------------------------------------------------------------------------------
/app/routes/profile.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "remix";
2 |
3 | export default function ProfileRoute() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/remix.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('@remix-run/dev/config').AppConfig}
3 | */
4 | module.exports = {
5 | appDirectory: "app",
6 | assetsBuildDirectory: "public/build",
7 | publicPath: "/build/",
8 | serverBuildDirectory: "build",
9 | devServerPort: 8002,
10 | ignoredRouteFiles: [".*"]
11 | };
12 |
--------------------------------------------------------------------------------
/app/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "remix";
2 |
3 | export default function Index() {
4 | return (
5 |
6 |
Welcome to Remix with Hasura
7 |
8 | Profile
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/app/routes/logout.tsx:
--------------------------------------------------------------------------------
1 | import type { ActionFunction, LoaderFunction } from "remix";
2 | import { redirect } from "remix";
3 | import { logout } from "~/utils/session.server";
4 |
5 | export let action: ActionFunction = async ({ request }) => {
6 | return logout(request);
7 | };
8 |
9 | export let loader: LoaderFunction = async () => {
10 | return redirect("/");
11 | };
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "lib": ["DOM", "DOM.Iterable", "ES2019"],
5 | "isolatedModules": true,
6 | "esModuleInterop": true,
7 | "jsx": "react-jsx",
8 | "moduleResolution": "node",
9 | "resolveJsonModule": true,
10 | "target": "ES2019",
11 | "strict": true,
12 | "baseUrl": ".",
13 | "paths": {
14 | "~/*": ["./app/*"]
15 | },
16 |
17 | // Remix takes care of building everything in `remix build`.
18 | "noEmit": true
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/entry.server.tsx:
--------------------------------------------------------------------------------
1 | import { renderToString } from "react-dom/server";
2 | import { RemixServer } from "remix";
3 | import type { EntryContext } from "remix";
4 |
5 | export default function handleRequest(
6 | request: Request,
7 | responseStatusCode: number,
8 | responseHeaders: Headers,
9 | remixContext: EntryContext
10 | ) {
11 | const markup = renderToString(
12 |
13 | );
14 |
15 | responseHeaders.set("Content-Type", "text/html");
16 |
17 | return new Response("" + markup, {
18 | status: responseStatusCode,
19 | headers: responseHeaders
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | # fly.toml file generated for red-sun-3656 on 2021-12-29T10:01:25-07:00
2 |
3 | app = "red-sun-3656"
4 |
5 | kill_signal = "SIGINT"
6 | kill_timeout = 5
7 | processes = []
8 |
9 | [env]
10 | PORT = "8080"
11 |
12 | [experimental]
13 | allowed_public_ports = []
14 | auto_rollback = true
15 |
16 | [[services]]
17 | http_checks = []
18 | internal_port = 8080
19 | processes = ["app"]
20 | protocol = "tcp"
21 | script_checks = []
22 |
23 | [services.concurrency]
24 | hard_limit = 25
25 | soft_limit = 20
26 | type = "connections"
27 |
28 | [[services.ports]]
29 | handlers = ["http"]
30 | port = 80
31 |
32 | [[services.ports]]
33 | handlers = ["tls", "http"]
34 | port = 443
35 |
36 | [[services.tcp_checks]]
37 | grace_period = "1s"
38 | interval = "15s"
39 | restart_limit = 0
40 | timeout = "2s"
41 |
--------------------------------------------------------------------------------
/app/root.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Links,
3 | LiveReload,
4 | Meta,
5 | Outlet,
6 | Scripts,
7 | ScrollRestoration,
8 | } from "remix";
9 | import type { MetaFunction, LinksFunction } from "remix";
10 | import styles from "~/styles/tailwind.css";
11 |
12 | export const meta: MetaFunction = () => {
13 | return { title: "New Remix App" };
14 | };
15 |
16 | export let links: LinksFunction = () => {
17 | return [{ rel: "stylesheet", href: styles }];
18 | };
19 |
20 | export default function App() {
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {process.env.NODE_ENV === "development" && }
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to Remix!
2 |
3 | - [Remix Docs](https://remix.run/docs)
4 |
5 | ## Development
6 |
7 | From your terminal:
8 |
9 | ```sh
10 | npm run dev
11 | ```
12 |
13 | This starts your app in development mode, rebuilding assets on file changes.
14 |
15 | ## Deployment
16 |
17 | First, build your app for production:
18 |
19 | ```sh
20 | npm run build
21 | ```
22 |
23 | Then run the app in production mode:
24 |
25 | ```sh
26 | npm start
27 | ```
28 |
29 | Now you'll need to pick a host to deploy it to.
30 |
31 | ### DIY
32 |
33 | If you're familiar with deploying node applications, the built-in Remix app server is production-ready.
34 |
35 | Make sure to deploy the output of `remix build`
36 |
37 | - `build/`
38 | - `public/build/`
39 |
40 | ### Using a Template
41 |
42 | When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server.
43 |
44 | ```sh
45 | cd ..
46 | # create a new project, and pick a pre-configured host
47 | npx create-remix@latest
48 | cd my-new-remix-app
49 | # remove the new project's app (not the old one!)
50 | rm -rf app
51 | # copy your app over
52 | cp -R ../my-old-remix-app/app app
53 | ```
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "remix-app-template",
4 | "description": "",
5 | "license": "",
6 | "scripts": {
7 | "build": "npm run build:css && remix build",
8 | "build:css": "tailwindcss -o ./app/styles/tailwind.css",
9 | "dev": "concurrently \"npm run dev:css\" \"node -r dotenv/config node_modules/.bin/remix dev\"",
10 | "dev:css": "tailwindcss -o ./app/styles/tailwind.css --watch",
11 | "postinstall": "remix setup node",
12 | "deploy": "fly deploy --remote-only",
13 | "start": "remix-serve build"
14 | },
15 | "dependencies": {
16 | "@headlessui/react": "^1.4.2",
17 | "@heroicons/react": "^1.0.5",
18 | "@remix-run/react": "^1.1.1",
19 | "@remix-run/serve": "^1.1.1",
20 | "@tailwindcss/forms": "^0.4.0",
21 | "bcrypt": "^5.0.1",
22 | "dotenv": "^10.0.0",
23 | "graphql": "^16.2.0",
24 | "graphql-request": "^3.7.0",
25 | "react": "^17.0.2",
26 | "react-dom": "^17.0.2",
27 | "remix": "^1.1.1"
28 | },
29 | "devDependencies": {
30 | "@remix-run/dev": "^1.1.1",
31 | "@types/bcrypt": "^5.0.0",
32 | "@types/react": "^17.0.24",
33 | "@types/react-dom": "^17.0.9",
34 | "concurrently": "^6.5.1",
35 | "tailwindcss": "^3.0.8",
36 | "typescript": "^4.1.2"
37 | },
38 | "engines": {
39 | "node": ">=14"
40 | },
41 | "sideEffects": false
42 | }
43 |
--------------------------------------------------------------------------------
/app/routes/api/on_item_insert.ts:
--------------------------------------------------------------------------------
1 | import type { ActionFunction } from "remix";
2 | import { hasuraAdminClient } from "~/utils/hasura.server";
3 | import { gql } from "graphql-request";
4 |
5 | const INSERT_ITEM_LOG = gql`
6 | mutation InsertItemLog($object: item_insert_log_insert_input!) {
7 | insert_item_insert_log_one(object: $object) {
8 | id
9 | }
10 | }
11 | `;
12 |
13 | export const action: ActionFunction = async ({ request }) => {
14 | if (!validateRequest(request)) {
15 | return new Response(null, { status: 401 });
16 | }
17 |
18 | try {
19 | if (request.method === "POST") {
20 | /* handle "POST" */
21 | const requestBody = await request.json();
22 | const itemData = requestBody.event?.data?.new;
23 |
24 | if (!itemData) {
25 | return new Response("Invalid request", { status: 500 });
26 | }
27 |
28 | hasuraAdminClient.request(INSERT_ITEM_LOG, {
29 | object: { item_json: itemData },
30 | });
31 |
32 | return new Response(null, { status: 200 });
33 | }
34 |
35 | return new Response("Request method not supported", { status: 400 });
36 | } catch (err) {
37 | console.log("err: ", err);
38 | return new Response(JSON.stringify(err), { status: 500 });
39 | }
40 | };
41 |
42 | function validateRequest(request: Request) {
43 | return request.headers.get("api-secret") === process.env.API_SECRET;
44 | }
45 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # base node image
2 | FROM node:16-bullseye-slim as base
3 |
4 | # Install openssl for Prisma
5 | RUN apt-get update && apt-get install -y openssl
6 |
7 | # Install all node_modules, including dev dependencies
8 | FROM base as deps
9 |
10 | RUN mkdir /app
11 | WORKDIR /app
12 |
13 | ADD package.json package-lock.json ./
14 | RUN npm install --production=false
15 |
16 | # Setup production node_modules
17 | FROM base as production-deps
18 |
19 | RUN mkdir /app
20 | WORKDIR /app
21 |
22 | COPY --from=deps /app/node_modules /app/node_modules
23 | ADD package.json package-lock.json ./
24 | RUN npm prune --production
25 |
26 | # Build the app
27 | FROM base as build
28 |
29 | ENV NODE_ENV=production
30 |
31 | RUN mkdir /app
32 | WORKDIR /app
33 |
34 | COPY --from=deps /app/node_modules /app/node_modules
35 |
36 | # If we're using Prisma, uncomment to cache the prisma schema
37 | # ADD prisma .
38 | # RUN npx prisma generate
39 |
40 | ADD . .
41 | RUN npm run build
42 |
43 | # Finally, build the production image with minimal footprint
44 | FROM base
45 |
46 | ENV NODE_ENV=production
47 |
48 | RUN mkdir /app
49 | WORKDIR /app
50 |
51 | COPY --from=production-deps /app/node_modules /app/node_modules
52 |
53 | # Uncomment if using Prisma
54 | # COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma
55 |
56 | COPY --from=build /app/build /app/build
57 | COPY --from=build /app/public /app/public
58 | ADD . .
59 |
60 | CMD ["npm", "run", "start"]
61 |
--------------------------------------------------------------------------------
/app/utils/session.server.ts:
--------------------------------------------------------------------------------
1 | import bcrypt from "bcrypt";
2 | import { createCookieSessionStorage, redirect } from "remix";
3 | import { getUserById, getUserByUsername, createUser } from "./hasura.server";
4 |
5 | type LoginForm = {
6 | username: string;
7 | password: string;
8 | };
9 |
10 | export async function register({ username, password }: LoginForm) {
11 | let passwordHash = await bcrypt.hash(password, 10);
12 |
13 | try {
14 | return await createUser(username, passwordHash);
15 | } catch (err) {
16 | return null;
17 | }
18 | }
19 |
20 | export async function login({ username, password }: LoginForm) {
21 | const user = await getUserByUsername(username);
22 | if (!user) return null;
23 | const isCorrectPassword = await bcrypt.compare(password, user.password_hash);
24 | if (!isCorrectPassword) return null;
25 | return user;
26 | }
27 |
28 | let sessionSecret = process.env.SESSION_SECRET;
29 | if (!sessionSecret) {
30 | throw new Error("SESSION_SECRET must be set");
31 | }
32 |
33 | let { getSession, commitSession, destroySession } = createCookieSessionStorage({
34 | cookie: {
35 | name: "RemixHasuraSession",
36 | secure: true,
37 | secrets: [sessionSecret],
38 | sameSite: "lax",
39 | path: "/",
40 | maxAge: 60 * 60 * 24 * 30,
41 | httpOnly: true,
42 | },
43 | });
44 |
45 | export function getUserSession(request: Request) {
46 | return getSession(request.headers.get("Cookie"));
47 | }
48 |
49 | export async function getUserId(request: Request) {
50 | let session = await getUserSession(request);
51 | let userId = session.get("userId");
52 | if (!userId || typeof userId !== "string") throw redirect("/login");
53 | return userId;
54 | }
55 |
56 | export async function getUser(request: Request) {
57 | let userId = await getUserId(request);
58 | if (typeof userId !== "string") return null;
59 |
60 | try {
61 | const user = await getUserById(userId);
62 | return user;
63 | } catch {
64 | throw logout(request);
65 | }
66 | }
67 |
68 | export async function logout(request: Request) {
69 | let session = await getSession(request.headers.get("Cookie"));
70 | return redirect("/login", {
71 | headers: { "Set-Cookie": await destroySession(session) },
72 | });
73 | }
74 |
75 | export async function createUserSession(userId: string, redirectTo: string) {
76 | let session = await getSession();
77 | session.set("userId", userId);
78 | return redirect(redirectTo, {
79 | headers: { "Set-Cookie": await commitSession(session) },
80 | });
81 | }
82 |
--------------------------------------------------------------------------------
/app/routes/profile/index.tsx:
--------------------------------------------------------------------------------
1 | import { useLoaderData, Form } from "remix";
2 | import type { LoaderFunction } from "remix";
3 | import { gql } from "graphql-request";
4 | import { getUser } from "~/utils/session.server";
5 | import { User, getHasuraClient } from "~/utils/hasura.server";
6 |
7 | const GET_USER_ITEMS = gql`
8 | query GetItems {
9 | item {
10 | id
11 | name
12 | }
13 | }
14 | `;
15 |
16 | type Item = {
17 | id: number;
18 | name: string;
19 | };
20 | type LoaderData = {
21 | user: User;
22 | items: Array- ;
23 | };
24 |
25 | export const loader: LoaderFunction = async ({ request }) => {
26 | const user = await getUser(request);
27 |
28 | if (user) {
29 | const { item } = await getHasuraClient(user.id).request(GET_USER_ITEMS);
30 |
31 | return {
32 | user,
33 | items: item,
34 | };
35 | }
36 | };
37 |
38 | export default function ProfileRoute() {
39 | const data = useLoaderData();
40 |
41 | return (
42 |
43 |
44 |
45 | Welcome,{" "}
46 | {data?.user?.username}
47 |
48 |
49 |
74 |
75 |
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/app/utils/hasura.server.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLClient, gql } from "graphql-request";
2 |
3 | const GRAPHQL_ENDPOINT = process.env.GRAPHQL_ENDPOINT || "";
4 | const GRAPHQL_ADMIN_SECRET = process.env.GRAPHQL_ADMIN_SECRET || "";
5 |
6 | if (!GRAPHQL_ENDPOINT || !GRAPHQL_ADMIN_SECRET) {
7 | throw new Error("GRAPHQL_ENDPOINT & GRAPHQL_ADMIN_SECRET must be set");
8 | }
9 |
10 | let hasuraClient: GraphQLClient | undefined;
11 | function getHasuraClient(userId: string) {
12 | if (!hasuraClient) {
13 | hasuraClient = new GraphQLClient(GRAPHQL_ENDPOINT, {
14 | headers: {
15 | "x-hasura-admin-secret": GRAPHQL_ADMIN_SECRET,
16 | "x-hasura-role": "user",
17 | },
18 | });
19 | }
20 |
21 | hasuraClient.setHeader("x-hasura-user-id", userId);
22 |
23 | return hasuraClient;
24 | }
25 |
26 | const hasuraAdminClient: GraphQLClient = new GraphQLClient(GRAPHQL_ENDPOINT, {
27 | headers: {
28 | "x-hasura-admin-secret": GRAPHQL_ADMIN_SECRET,
29 | },
30 | });
31 |
32 | type User = {
33 | id: string;
34 | username: string;
35 | password_hash: string;
36 | };
37 |
38 | export async function getUserById(id: string): Promise {
39 | const { user_by_pk } = await hasuraAdminClient.request(
40 | gql`
41 | query GetUserByPk($id: uuid!) {
42 | user_by_pk(id: $id) {
43 | id
44 | username
45 | password_hash
46 | }
47 | }
48 | `,
49 | { id }
50 | );
51 |
52 | if (!user_by_pk) {
53 | throw new Error("Invalid user id");
54 | }
55 |
56 | return user_by_pk;
57 | }
58 |
59 | export async function getUserByUsername(
60 | username: string
61 | ): Promise {
62 | const { user } = await hasuraAdminClient.request(
63 | gql`
64 | query GetUserByUsername($username: String!) {
65 | user(where: { username: { _eq: $username } }) {
66 | id
67 | username
68 | password_hash
69 | }
70 | }
71 | `,
72 | { username }
73 | );
74 |
75 | if (!user || !user.length) {
76 | return null;
77 | }
78 |
79 | return user[0];
80 | }
81 |
82 | export async function createUser(
83 | username: string,
84 | password_hash: string
85 | ): Promise {
86 | const { insert_user_one } = await hasuraAdminClient.request(
87 | gql`
88 | mutation CreateUser($object: user_insert_input!) {
89 | insert_user_one(object: $object) {
90 | id
91 | username
92 | password_hash
93 | }
94 | }
95 | `,
96 | {
97 | object: {
98 | password_hash,
99 | username,
100 | },
101 | }
102 | );
103 |
104 | return insert_user_one;
105 | }
106 |
107 | export { getHasuraClient, hasuraAdminClient };
108 | export type { User };
109 |
--------------------------------------------------------------------------------
/app/routes/login.tsx:
--------------------------------------------------------------------------------
1 | import { ActionFunction, HeadersFunction, MetaFunction } from "remix";
2 | import { useActionData, Form } from "remix";
3 | import { login, createUserSession, register } from "~/utils/session.server";
4 | import { getUserByUsername } from "~/utils/hasura.server";
5 | import {
6 | LockClosedIcon,
7 | XCircleIcon,
8 | ExclamationIcon,
9 | } from "@heroicons/react/solid";
10 |
11 | export let meta: MetaFunction = () => {
12 | return {
13 | title: "Remix Hasura Fly | Login",
14 | description: "Login to authenticate with Hasura!",
15 | };
16 | };
17 |
18 | export let headers: HeadersFunction = () => {
19 | return {
20 | "Cache-Control": `public, max-age=${60 * 10}, s-maxage=${
21 | 60 * 60 * 24 * 30
22 | }`,
23 | };
24 | };
25 |
26 | function validateUsername(username: unknown) {
27 | if (typeof username !== "string" || username.length < 3) {
28 | return `Usernames must be at least 3 characters long`;
29 | }
30 | }
31 |
32 | function validatePassword(password: unknown) {
33 | if (typeof password !== "string" || password.length < 6) {
34 | return `Passwords must be at least 6 characters long`;
35 | }
36 | }
37 |
38 | type ActionData = {
39 | formError?: string;
40 | fieldErrors?: { username: string | undefined; password: string | undefined };
41 | fields?: { loginType: string; username: string; password: string };
42 | };
43 |
44 | export let action: ActionFunction = async ({
45 | request,
46 | }): Promise => {
47 | let { loginType, username, password } = Object.fromEntries(
48 | await request.formData()
49 | );
50 | if (
51 | typeof loginType !== "string" ||
52 | typeof username !== "string" ||
53 | typeof password !== "string"
54 | ) {
55 | return { formError: `Form not submitted correctly.` };
56 | }
57 |
58 | let fields = { loginType, username, password };
59 | let fieldErrors = {
60 | username: validateUsername(username),
61 | password: validatePassword(password),
62 | };
63 | if (Object.values(fieldErrors).some(Boolean)) return { fieldErrors, fields };
64 |
65 | switch (loginType) {
66 | case "login": {
67 | const user = await login({ username, password });
68 | if (!user) {
69 | return {
70 | fields,
71 | formError: `Username/Password combination is incorrect`,
72 | };
73 | }
74 | return createUserSession(user.id, "/profile");
75 | }
76 | case "register": {
77 | let userExists = await getUserByUsername(username);
78 | if (userExists) {
79 | return {
80 | fields,
81 | formError: `User with username ${username} already exists`,
82 | };
83 | }
84 | const user = await register({ username, password });
85 | if (!user) {
86 | return {
87 | fields,
88 | formError: `Something went wrong trying to create a new user.`,
89 | };
90 | }
91 | return createUserSession(user.id, "/profile");
92 | }
93 | default: {
94 | return { fields, formError: `Login type invalid` };
95 | }
96 | }
97 | };
98 |
99 | export default function Login() {
100 | const actionData = useActionData();
101 |
102 | return (
103 |
104 |
105 |
106 |
107 | Sign in to your account
108 |
109 |
110 |
253 |
254 |
255 | );
256 | }
257 |
--------------------------------------------------------------------------------
/app/styles/tailwind.css:
--------------------------------------------------------------------------------
1 | /*
2 | ! tailwindcss v3.0.8 | MIT License | https://tailwindcss.com
3 | */
4 |
5 | /*
6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
8 | */
9 |
10 | *,
11 | ::before,
12 | ::after {
13 | box-sizing: border-box;
14 | /* 1 */
15 | border-width: 0;
16 | /* 2 */
17 | border-style: solid;
18 | /* 2 */
19 | border-color: currentColor;
20 | /* 2 */
21 | }
22 |
23 | ::before,
24 | ::after {
25 | --tw-content: '';
26 | }
27 |
28 | /*
29 | 1. Use a consistent sensible line-height in all browsers.
30 | 2. Prevent adjustments of font size after orientation changes in iOS.
31 | 3. Use a more readable tab size.
32 | 4. Use the user's configured `sans` font-family by default.
33 | */
34 |
35 | html {
36 | line-height: 1.5;
37 | /* 1 */
38 | -webkit-text-size-adjust: 100%;
39 | /* 2 */
40 | -moz-tab-size: 4;
41 | /* 3 */
42 | -o-tab-size: 4;
43 | tab-size: 4;
44 | /* 3 */
45 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
46 | /* 4 */
47 | }
48 |
49 | /*
50 | 1. Remove the margin in all browsers.
51 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
52 | */
53 |
54 | body {
55 | margin: 0;
56 | /* 1 */
57 | line-height: inherit;
58 | /* 2 */
59 | }
60 |
61 | /*
62 | 1. Add the correct height in Firefox.
63 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
64 | 3. Ensure horizontal rules are visible by default.
65 | */
66 |
67 | hr {
68 | height: 0;
69 | /* 1 */
70 | color: inherit;
71 | /* 2 */
72 | border-top-width: 1px;
73 | /* 3 */
74 | }
75 |
76 | /*
77 | Add the correct text decoration in Chrome, Edge, and Safari.
78 | */
79 |
80 | abbr:where([title]) {
81 | -webkit-text-decoration: underline dotted;
82 | text-decoration: underline dotted;
83 | }
84 |
85 | /*
86 | Remove the default font size and weight for headings.
87 | */
88 |
89 | h1,
90 | h2,
91 | h3,
92 | h4,
93 | h5,
94 | h6 {
95 | font-size: inherit;
96 | font-weight: inherit;
97 | }
98 |
99 | /*
100 | Reset links to optimize for opt-in styling instead of opt-out.
101 | */
102 |
103 | a {
104 | color: inherit;
105 | text-decoration: inherit;
106 | }
107 |
108 | /*
109 | Add the correct font weight in Edge and Safari.
110 | */
111 |
112 | b,
113 | strong {
114 | font-weight: bolder;
115 | }
116 |
117 | /*
118 | 1. Use the user's configured `mono` font family by default.
119 | 2. Correct the odd `em` font sizing in all browsers.
120 | */
121 |
122 | code,
123 | kbd,
124 | samp,
125 | pre {
126 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
127 | /* 1 */
128 | font-size: 1em;
129 | /* 2 */
130 | }
131 |
132 | /*
133 | Add the correct font size in all browsers.
134 | */
135 |
136 | small {
137 | font-size: 80%;
138 | }
139 |
140 | /*
141 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
142 | */
143 |
144 | sub,
145 | sup {
146 | font-size: 75%;
147 | line-height: 0;
148 | position: relative;
149 | vertical-align: baseline;
150 | }
151 |
152 | sub {
153 | bottom: -0.25em;
154 | }
155 |
156 | sup {
157 | top: -0.5em;
158 | }
159 |
160 | /*
161 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
162 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
163 | 3. Remove gaps between table borders by default.
164 | */
165 |
166 | table {
167 | text-indent: 0;
168 | /* 1 */
169 | border-color: inherit;
170 | /* 2 */
171 | border-collapse: collapse;
172 | /* 3 */
173 | }
174 |
175 | /*
176 | 1. Change the font styles in all browsers.
177 | 2. Remove the margin in Firefox and Safari.
178 | 3. Remove default padding in all browsers.
179 | */
180 |
181 | button,
182 | input,
183 | optgroup,
184 | select,
185 | textarea {
186 | font-family: inherit;
187 | /* 1 */
188 | font-size: 100%;
189 | /* 1 */
190 | line-height: inherit;
191 | /* 1 */
192 | color: inherit;
193 | /* 1 */
194 | margin: 0;
195 | /* 2 */
196 | padding: 0;
197 | /* 3 */
198 | }
199 |
200 | /*
201 | Remove the inheritance of text transform in Edge and Firefox.
202 | */
203 |
204 | button,
205 | select {
206 | text-transform: none;
207 | }
208 |
209 | /*
210 | 1. Correct the inability to style clickable types in iOS and Safari.
211 | 2. Remove default button styles.
212 | */
213 |
214 | button,
215 | [type='button'],
216 | [type='reset'],
217 | [type='submit'] {
218 | -webkit-appearance: button;
219 | /* 1 */
220 | background-color: transparent;
221 | /* 2 */
222 | background-image: none;
223 | /* 2 */
224 | }
225 |
226 | /*
227 | Use the modern Firefox focus style for all focusable elements.
228 | */
229 |
230 | :-moz-focusring {
231 | outline: auto;
232 | }
233 |
234 | /*
235 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
236 | */
237 |
238 | :-moz-ui-invalid {
239 | box-shadow: none;
240 | }
241 |
242 | /*
243 | Add the correct vertical alignment in Chrome and Firefox.
244 | */
245 |
246 | progress {
247 | vertical-align: baseline;
248 | }
249 |
250 | /*
251 | Correct the cursor style of increment and decrement buttons in Safari.
252 | */
253 |
254 | ::-webkit-inner-spin-button,
255 | ::-webkit-outer-spin-button {
256 | height: auto;
257 | }
258 |
259 | /*
260 | 1. Correct the odd appearance in Chrome and Safari.
261 | 2. Correct the outline style in Safari.
262 | */
263 |
264 | [type='search'] {
265 | -webkit-appearance: textfield;
266 | /* 1 */
267 | outline-offset: -2px;
268 | /* 2 */
269 | }
270 |
271 | /*
272 | Remove the inner padding in Chrome and Safari on macOS.
273 | */
274 |
275 | ::-webkit-search-decoration {
276 | -webkit-appearance: none;
277 | }
278 |
279 | /*
280 | 1. Correct the inability to style clickable types in iOS and Safari.
281 | 2. Change font properties to `inherit` in Safari.
282 | */
283 |
284 | ::-webkit-file-upload-button {
285 | -webkit-appearance: button;
286 | /* 1 */
287 | font: inherit;
288 | /* 2 */
289 | }
290 |
291 | /*
292 | Add the correct display in Chrome and Safari.
293 | */
294 |
295 | summary {
296 | display: list-item;
297 | }
298 |
299 | /*
300 | Removes the default spacing and border for appropriate elements.
301 | */
302 |
303 | blockquote,
304 | dl,
305 | dd,
306 | h1,
307 | h2,
308 | h3,
309 | h4,
310 | h5,
311 | h6,
312 | hr,
313 | figure,
314 | p,
315 | pre {
316 | margin: 0;
317 | }
318 |
319 | fieldset {
320 | margin: 0;
321 | padding: 0;
322 | }
323 |
324 | legend {
325 | padding: 0;
326 | }
327 |
328 | ol,
329 | ul,
330 | menu {
331 | list-style: none;
332 | margin: 0;
333 | padding: 0;
334 | }
335 |
336 | /*
337 | Prevent resizing textareas horizontally by default.
338 | */
339 |
340 | textarea {
341 | resize: vertical;
342 | }
343 |
344 | /*
345 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
346 | 2. Set the default placeholder color to the user's configured gray 400 color.
347 | */
348 |
349 | input::-moz-placeholder, textarea::-moz-placeholder {
350 | opacity: 1;
351 | /* 1 */
352 | color: #9ca3af;
353 | /* 2 */
354 | }
355 |
356 | input:-ms-input-placeholder, textarea:-ms-input-placeholder {
357 | opacity: 1;
358 | /* 1 */
359 | color: #9ca3af;
360 | /* 2 */
361 | }
362 |
363 | input::placeholder,
364 | textarea::placeholder {
365 | opacity: 1;
366 | /* 1 */
367 | color: #9ca3af;
368 | /* 2 */
369 | }
370 |
371 | /*
372 | Set the default cursor for buttons.
373 | */
374 |
375 | button,
376 | [role="button"] {
377 | cursor: pointer;
378 | }
379 |
380 | /*
381 | Make sure disabled buttons don't get the pointer cursor.
382 | */
383 |
384 | :disabled {
385 | cursor: default;
386 | }
387 |
388 | /*
389 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
390 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
391 | This can trigger a poorly considered lint error in some tools but is included by design.
392 | */
393 |
394 | img,
395 | svg,
396 | video,
397 | canvas,
398 | audio,
399 | iframe,
400 | embed,
401 | object {
402 | display: block;
403 | /* 1 */
404 | vertical-align: middle;
405 | /* 2 */
406 | }
407 |
408 | /*
409 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
410 | */
411 |
412 | img,
413 | video {
414 | max-width: 100%;
415 | height: auto;
416 | }
417 |
418 | /*
419 | Ensure the default browser behavior of the `hidden` attribute.
420 | */
421 |
422 | [hidden] {
423 | display: none;
424 | }
425 |
426 | [type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
427 | -webkit-appearance: none;
428 | -moz-appearance: none;
429 | appearance: none;
430 | background-color: #fff;
431 | border-color: #6b7280;
432 | border-width: 1px;
433 | border-radius: 0px;
434 | padding-top: 0.5rem;
435 | padding-right: 0.75rem;
436 | padding-bottom: 0.5rem;
437 | padding-left: 0.75rem;
438 | font-size: 1rem;
439 | line-height: 1.5rem;
440 | --tw-shadow: 0 0 #0000;
441 | }
442 |
443 | [type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
444 | outline: 2px solid transparent;
445 | outline-offset: 2px;
446 | --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
447 | --tw-ring-offset-width: 0px;
448 | --tw-ring-offset-color: #fff;
449 | --tw-ring-color: #2563eb;
450 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
451 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
452 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
453 | border-color: #2563eb;
454 | }
455 |
456 | input::-moz-placeholder, textarea::-moz-placeholder {
457 | color: #6b7280;
458 | opacity: 1;
459 | }
460 |
461 | input:-ms-input-placeholder, textarea:-ms-input-placeholder {
462 | color: #6b7280;
463 | opacity: 1;
464 | }
465 |
466 | input::placeholder,textarea::placeholder {
467 | color: #6b7280;
468 | opacity: 1;
469 | }
470 |
471 | ::-webkit-datetime-edit-fields-wrapper {
472 | padding: 0;
473 | }
474 |
475 | ::-webkit-date-and-time-value {
476 | min-height: 1.5em;
477 | }
478 |
479 | select {
480 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
481 | background-position: right 0.5rem center;
482 | background-repeat: no-repeat;
483 | background-size: 1.5em 1.5em;
484 | padding-right: 2.5rem;
485 | -webkit-print-color-adjust: exact;
486 | color-adjust: exact;
487 | }
488 |
489 | [multiple] {
490 | background-image: initial;
491 | background-position: initial;
492 | background-repeat: unset;
493 | background-size: initial;
494 | padding-right: 0.75rem;
495 | -webkit-print-color-adjust: unset;
496 | color-adjust: unset;
497 | }
498 |
499 | [type='checkbox'],[type='radio'] {
500 | -webkit-appearance: none;
501 | -moz-appearance: none;
502 | appearance: none;
503 | padding: 0;
504 | -webkit-print-color-adjust: exact;
505 | color-adjust: exact;
506 | display: inline-block;
507 | vertical-align: middle;
508 | background-origin: border-box;
509 | -webkit-user-select: none;
510 | -moz-user-select: none;
511 | -ms-user-select: none;
512 | user-select: none;
513 | flex-shrink: 0;
514 | height: 1rem;
515 | width: 1rem;
516 | color: #2563eb;
517 | background-color: #fff;
518 | border-color: #6b7280;
519 | border-width: 1px;
520 | --tw-shadow: 0 0 #0000;
521 | }
522 |
523 | [type='checkbox'] {
524 | border-radius: 0px;
525 | }
526 |
527 | [type='radio'] {
528 | border-radius: 100%;
529 | }
530 |
531 | [type='checkbox']:focus,[type='radio']:focus {
532 | outline: 2px solid transparent;
533 | outline-offset: 2px;
534 | --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
535 | --tw-ring-offset-width: 2px;
536 | --tw-ring-offset-color: #fff;
537 | --tw-ring-color: #2563eb;
538 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
539 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
540 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
541 | }
542 |
543 | [type='checkbox']:checked,[type='radio']:checked {
544 | border-color: transparent;
545 | background-color: currentColor;
546 | background-size: 100% 100%;
547 | background-position: center;
548 | background-repeat: no-repeat;
549 | }
550 |
551 | [type='checkbox']:checked {
552 | background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
553 | }
554 |
555 | [type='radio']:checked {
556 | background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
557 | }
558 |
559 | [type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
560 | border-color: transparent;
561 | background-color: currentColor;
562 | }
563 |
564 | [type='checkbox']:indeterminate {
565 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
566 | border-color: transparent;
567 | background-color: currentColor;
568 | background-size: 100% 100%;
569 | background-position: center;
570 | background-repeat: no-repeat;
571 | }
572 |
573 | [type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
574 | border-color: transparent;
575 | background-color: currentColor;
576 | }
577 |
578 | [type='file'] {
579 | background: unset;
580 | border-color: inherit;
581 | border-width: 0;
582 | border-radius: 0;
583 | padding: 0;
584 | font-size: unset;
585 | line-height: inherit;
586 | }
587 |
588 | [type='file']:focus {
589 | outline: 1px auto -webkit-focus-ring-color;
590 | }
591 |
592 | *, ::before, ::after {
593 | --tw-border-opacity: 1;
594 | border-color: rgb(229 231 235 / var(--tw-border-opacity));
595 | --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
596 | --tw-ring-offset-width: 0px;
597 | --tw-ring-offset-color: #fff;
598 | --tw-ring-color: rgb(59 130 246 / 0.5);
599 | --tw-ring-offset-shadow: 0 0 #0000;
600 | --tw-ring-shadow: 0 0 #0000;
601 | --tw-shadow: 0 0 #0000;
602 | --tw-shadow-colored: 0 0 #0000;
603 | }
604 |
605 | .sr-only {
606 | position: absolute;
607 | width: 1px;
608 | height: 1px;
609 | padding: 0;
610 | margin: -1px;
611 | overflow: hidden;
612 | clip: rect(0, 0, 0, 0);
613 | white-space: nowrap;
614 | border-width: 0;
615 | }
616 |
617 | .absolute {
618 | position: absolute;
619 | }
620 |
621 | .relative {
622 | position: relative;
623 | }
624 |
625 | .inset-y-0 {
626 | top: 0px;
627 | bottom: 0px;
628 | }
629 |
630 | .left-0 {
631 | left: 0px;
632 | }
633 |
634 | .mx-auto {
635 | margin-left: auto;
636 | margin-right: auto;
637 | }
638 |
639 | .mt-6 {
640 | margin-top: 1.5rem;
641 | }
642 |
643 | .mt-8 {
644 | margin-top: 2rem;
645 | }
646 |
647 | .ml-3 {
648 | margin-left: 0.75rem;
649 | }
650 |
651 | .mt-2 {
652 | margin-top: 0.5rem;
653 | }
654 |
655 | .mt-20 {
656 | margin-top: 5rem;
657 | }
658 |
659 | .block {
660 | display: block;
661 | }
662 |
663 | .flex {
664 | display: flex;
665 | }
666 |
667 | .inline-flex {
668 | display: inline-flex;
669 | }
670 |
671 | .h-full {
672 | height: 100%;
673 | }
674 |
675 | .h-4 {
676 | height: 1rem;
677 | }
678 |
679 | .h-5 {
680 | height: 1.25rem;
681 | }
682 |
683 | .min-h-full {
684 | min-height: 100%;
685 | }
686 |
687 | .w-full {
688 | width: 100%;
689 | }
690 |
691 | .w-4 {
692 | width: 1rem;
693 | }
694 |
695 | .w-5 {
696 | width: 1.25rem;
697 | }
698 |
699 | .max-w-md {
700 | max-width: 28rem;
701 | }
702 |
703 | .max-w-4xl {
704 | max-width: 56rem;
705 | }
706 |
707 | .flex-shrink-0 {
708 | flex-shrink: 0;
709 | }
710 |
711 | .list-disc {
712 | list-style-type: disc;
713 | }
714 |
715 | .appearance-none {
716 | -webkit-appearance: none;
717 | -moz-appearance: none;
718 | appearance: none;
719 | }
720 |
721 | .items-start {
722 | align-items: flex-start;
723 | }
724 |
725 | .items-center {
726 | align-items: center;
727 | }
728 |
729 | .justify-center {
730 | justify-content: center;
731 | }
732 |
733 | .space-y-8 > :not([hidden]) ~ :not([hidden]) {
734 | --tw-space-y-reverse: 0;
735 | margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse)));
736 | margin-bottom: calc(2rem * var(--tw-space-y-reverse));
737 | }
738 |
739 | .space-y-6 > :not([hidden]) ~ :not([hidden]) {
740 | --tw-space-y-reverse: 0;
741 | margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
742 | margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
743 | }
744 |
745 | .space-y-4 > :not([hidden]) ~ :not([hidden]) {
746 | --tw-space-y-reverse: 0;
747 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
748 | margin-bottom: calc(1rem * var(--tw-space-y-reverse));
749 | }
750 |
751 | .-space-y-px > :not([hidden]) ~ :not([hidden]) {
752 | --tw-space-y-reverse: 0;
753 | margin-top: calc(-1px * calc(1 - var(--tw-space-y-reverse)));
754 | margin-bottom: calc(-1px * var(--tw-space-y-reverse));
755 | }
756 |
757 | .space-y-1 > :not([hidden]) ~ :not([hidden]) {
758 | --tw-space-y-reverse: 0;
759 | margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
760 | margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
761 | }
762 |
763 | .space-y-5 > :not([hidden]) ~ :not([hidden]) {
764 | --tw-space-y-reverse: 0;
765 | margin-top: calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));
766 | margin-bottom: calc(1.25rem * var(--tw-space-y-reverse));
767 | }
768 |
769 | .overflow-hidden {
770 | overflow: hidden;
771 | }
772 |
773 | .rounded-md {
774 | border-radius: 0.375rem;
775 | }
776 |
777 | .rounded-none {
778 | border-radius: 0px;
779 | }
780 |
781 | .rounded-lg {
782 | border-radius: 0.5rem;
783 | }
784 |
785 | .rounded {
786 | border-radius: 0.25rem;
787 | }
788 |
789 | .rounded-t-md {
790 | border-top-left-radius: 0.375rem;
791 | border-top-right-radius: 0.375rem;
792 | }
793 |
794 | .rounded-b-md {
795 | border-bottom-right-radius: 0.375rem;
796 | border-bottom-left-radius: 0.375rem;
797 | }
798 |
799 | .border {
800 | border-width: 1px;
801 | }
802 |
803 | .border-gray-300 {
804 | --tw-border-opacity: 1;
805 | border-color: rgb(209 213 219 / var(--tw-border-opacity));
806 | }
807 |
808 | .border-transparent {
809 | border-color: transparent;
810 | }
811 |
812 | .bg-gray-50 {
813 | --tw-bg-opacity: 1;
814 | background-color: rgb(249 250 251 / var(--tw-bg-opacity));
815 | }
816 |
817 | .bg-red-50 {
818 | --tw-bg-opacity: 1;
819 | background-color: rgb(254 242 242 / var(--tw-bg-opacity));
820 | }
821 |
822 | .bg-yellow-50 {
823 | --tw-bg-opacity: 1;
824 | background-color: rgb(254 252 232 / var(--tw-bg-opacity));
825 | }
826 |
827 | .bg-indigo-600 {
828 | --tw-bg-opacity: 1;
829 | background-color: rgb(79 70 229 / var(--tw-bg-opacity));
830 | }
831 |
832 | .bg-white {
833 | --tw-bg-opacity: 1;
834 | background-color: rgb(255 255 255 / var(--tw-bg-opacity));
835 | }
836 |
837 | .p-4 {
838 | padding: 1rem;
839 | }
840 |
841 | .py-12 {
842 | padding-top: 3rem;
843 | padding-bottom: 3rem;
844 | }
845 |
846 | .px-4 {
847 | padding-left: 1rem;
848 | padding-right: 1rem;
849 | }
850 |
851 | .px-3 {
852 | padding-left: 0.75rem;
853 | padding-right: 0.75rem;
854 | }
855 |
856 | .py-2 {
857 | padding-top: 0.5rem;
858 | padding-bottom: 0.5rem;
859 | }
860 |
861 | .py-5 {
862 | padding-top: 1.25rem;
863 | padding-bottom: 1.25rem;
864 | }
865 |
866 | .pl-5 {
867 | padding-left: 1.25rem;
868 | }
869 |
870 | .pl-3 {
871 | padding-left: 0.75rem;
872 | }
873 |
874 | .text-center {
875 | text-align: center;
876 | }
877 |
878 | .text-3xl {
879 | font-size: 1.875rem;
880 | line-height: 2.25rem;
881 | }
882 |
883 | .text-sm {
884 | font-size: 0.875rem;
885 | line-height: 1.25rem;
886 | }
887 |
888 | .font-extrabold {
889 | font-weight: 800;
890 | }
891 |
892 | .font-medium {
893 | font-weight: 500;
894 | }
895 |
896 | .font-bold {
897 | font-weight: 700;
898 | }
899 |
900 | .leading-4 {
901 | line-height: 1rem;
902 | }
903 |
904 | .text-red-500 {
905 | --tw-text-opacity: 1;
906 | color: rgb(239 68 68 / var(--tw-text-opacity));
907 | }
908 |
909 | .text-blue-400 {
910 | --tw-text-opacity: 1;
911 | color: rgb(96 165 250 / var(--tw-text-opacity));
912 | }
913 |
914 | .text-gray-900 {
915 | --tw-text-opacity: 1;
916 | color: rgb(17 24 39 / var(--tw-text-opacity));
917 | }
918 |
919 | .text-indigo-600 {
920 | --tw-text-opacity: 1;
921 | color: rgb(79 70 229 / var(--tw-text-opacity));
922 | }
923 |
924 | .text-gray-700 {
925 | --tw-text-opacity: 1;
926 | color: rgb(55 65 81 / var(--tw-text-opacity));
927 | }
928 |
929 | .text-red-400 {
930 | --tw-text-opacity: 1;
931 | color: rgb(248 113 113 / var(--tw-text-opacity));
932 | }
933 |
934 | .text-red-800 {
935 | --tw-text-opacity: 1;
936 | color: rgb(153 27 27 / var(--tw-text-opacity));
937 | }
938 |
939 | .text-red-700 {
940 | --tw-text-opacity: 1;
941 | color: rgb(185 28 28 / var(--tw-text-opacity));
942 | }
943 |
944 | .text-yellow-400 {
945 | --tw-text-opacity: 1;
946 | color: rgb(250 204 21 / var(--tw-text-opacity));
947 | }
948 |
949 | .text-yellow-800 {
950 | --tw-text-opacity: 1;
951 | color: rgb(133 77 14 / var(--tw-text-opacity));
952 | }
953 |
954 | .text-yellow-700 {
955 | --tw-text-opacity: 1;
956 | color: rgb(161 98 7 / var(--tw-text-opacity));
957 | }
958 |
959 | .text-white {
960 | --tw-text-opacity: 1;
961 | color: rgb(255 255 255 / var(--tw-text-opacity));
962 | }
963 |
964 | .text-indigo-500 {
965 | --tw-text-opacity: 1;
966 | color: rgb(99 102 241 / var(--tw-text-opacity));
967 | }
968 |
969 | .placeholder-gray-500::-moz-placeholder {
970 | --tw-placeholder-opacity: 1;
971 | color: rgb(107 114 128 / var(--tw-placeholder-opacity));
972 | }
973 |
974 | .placeholder-gray-500:-ms-input-placeholder {
975 | --tw-placeholder-opacity: 1;
976 | color: rgb(107 114 128 / var(--tw-placeholder-opacity));
977 | }
978 |
979 | .placeholder-gray-500::placeholder {
980 | --tw-placeholder-opacity: 1;
981 | color: rgb(107 114 128 / var(--tw-placeholder-opacity));
982 | }
983 |
984 | .shadow-sm {
985 | --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
986 | --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
987 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
988 | }
989 |
990 | .shadow {
991 | --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
992 | --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
993 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
994 | }
995 |
996 | .hover\:bg-indigo-700:hover {
997 | --tw-bg-opacity: 1;
998 | background-color: rgb(67 56 202 / var(--tw-bg-opacity));
999 | }
1000 |
1001 | .hover\:underline:hover {
1002 | -webkit-text-decoration-line: underline;
1003 | text-decoration-line: underline;
1004 | }
1005 |
1006 | .focus\:z-10:focus {
1007 | z-index: 10;
1008 | }
1009 |
1010 | .focus\:border-indigo-500:focus {
1011 | --tw-border-opacity: 1;
1012 | border-color: rgb(99 102 241 / var(--tw-border-opacity));
1013 | }
1014 |
1015 | .focus\:outline-none:focus {
1016 | outline: 2px solid transparent;
1017 | outline-offset: 2px;
1018 | }
1019 |
1020 | .focus\:ring-2:focus {
1021 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
1022 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
1023 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
1024 | }
1025 |
1026 | .focus\:ring-indigo-500:focus {
1027 | --tw-ring-opacity: 1;
1028 | --tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity));
1029 | }
1030 |
1031 | .focus\:ring-offset-2:focus {
1032 | --tw-ring-offset-width: 2px;
1033 | }
1034 |
1035 | .group:hover .group-hover\:text-indigo-400 {
1036 | --tw-text-opacity: 1;
1037 | color: rgb(129 140 248 / var(--tw-text-opacity));
1038 | }
1039 |
1040 | @media (min-width: 640px) {
1041 | .sm\:flex {
1042 | display: flex;
1043 | }
1044 |
1045 | .sm\:items-center {
1046 | align-items: center;
1047 | }
1048 |
1049 | .sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) {
1050 | --tw-space-y-reverse: 0;
1051 | margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
1052 | margin-bottom: calc(0px * var(--tw-space-y-reverse));
1053 | }
1054 |
1055 | .sm\:space-x-10 > :not([hidden]) ~ :not([hidden]) {
1056 | --tw-space-x-reverse: 0;
1057 | margin-right: calc(2.5rem * var(--tw-space-x-reverse));
1058 | margin-left: calc(2.5rem * calc(1 - var(--tw-space-x-reverse)));
1059 | }
1060 |
1061 | .sm\:p-6 {
1062 | padding: 1.5rem;
1063 | }
1064 |
1065 | .sm\:px-6 {
1066 | padding-left: 1.5rem;
1067 | padding-right: 1.5rem;
1068 | }
1069 |
1070 | .sm\:text-sm {
1071 | font-size: 0.875rem;
1072 | line-height: 1.25rem;
1073 | }
1074 | }
1075 |
1076 | @media (min-width: 1024px) {
1077 | .lg\:px-8 {
1078 | padding-left: 2rem;
1079 | padding-right: 2rem;
1080 | }
1081 | }
--------------------------------------------------------------------------------